diff --git a/cinder/tests/unit/volume/drivers/hpe/test_hpe3par.py b/cinder/tests/unit/volume/drivers/hpe/test_hpe3par.py index 80f070503d1..7630960f2dd 100644 --- a/cinder/tests/unit/volume/drivers/hpe/test_hpe3par.py +++ b/cinder/tests/unit/volume/drivers/hpe/test_hpe3par.py @@ -254,6 +254,14 @@ class HPE3PARBaseDriver(test.TestCase): 'volume_type': None, 'volume_type_id': VOLUME_TYPE_ID_FLASH_CACHE} + volume_hos = {'name': VOLUME_NAME, + 'id': VOLUME_ID, + 'display_name': 'Foo Volume', + 'size': 2, + 'host': FAKE_CINDER_HOST, + 'volume_type': None, + 'volume_type_id': 'hos'} + snapshot = {'name': SNAPSHOT_NAME, 'id': SNAPSHOT_ID, 'user_id': USER_ID, @@ -342,6 +350,13 @@ class HPE3PARBaseDriver(test.TestCase): 'deleted_at': None, 'id': VOLUME_TYPE_ID_FLASH_CACHE} + volume_type_hos = {'name': 'hos', + 'deleted': False, + 'updated_at': None, + 'extra_specs': {'convert_to_base': False}, + 'deleted_at': None, + 'id': 'hos'} + flash_cache_3par_keys = {'flash_cache': 'true'} cpgs = [ @@ -3215,31 +3230,22 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver): def test_delete_snapshot_in_use(self): # setup_mock_client drive with default configuration # and return the mock HTTP 3PAR client - conf = { - 'getTask.return_value': { - 'status': 1}, - 'copyVolume.return_value': {'taskid': 1}, - 'getVolume.return_value': {} - } - mock_client = self.setup_driver(mock_conf=conf) + mock_client = self.setup_driver() with mock.patch.object(hpecommon.HPE3PARCommon, '_create_client') as mock_create_client: mock_create_client.return_value = mock_client - common = self.driver._login() + self.driver._login() volume = self.volume.copy() model_update = self.driver.create_volume_from_snapshot( - self.volume, + volume, self.snapshot) - self.assertIsNone(model_update) + self.assertEqual(model_update, {}) comment = Comment({ "snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31", "display_name": "Foo Volume", "volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7", }) - volume_name_3par = common._encode_name(volume['id']) - osv_matcher = 'osv-' + volume_name_3par - omv_matcher = 'omv-' + volume_name_3par expected = [ mock.call.createSnapshot( @@ -3247,13 +3253,7 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver): 'oss-L4I73ONuTci9Fd4ceij-MQ', { 'comment': comment, - 'readOnly': False}), - mock.call.copyVolume( - osv_matcher, omv_matcher, HPE3PAR_CPG, mock.ANY), - mock.call.getTask(mock.ANY), - mock.call.getVolume(osv_matcher), - mock.call.deleteVolume(osv_matcher), - mock.call.modifyVolume(omv_matcher, {'newName': osv_matcher})] + 'readOnly': False})] mock_client.assert_has_calls( self.standard_login + @@ -3290,31 +3290,22 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver): def test_create_volume_from_snapshot(self): # setup_mock_client drive with default configuration # and return the mock HTTP 3PAR client - conf = { - 'getTask.return_value': { - 'status': 1}, - 'copyVolume.return_value': {'taskid': 1}, - 'getVolume.return_value': {} - } - mock_client = self.setup_driver(mock_conf=conf) + mock_client = self.setup_driver() with mock.patch.object(hpecommon.HPE3PARCommon, '_create_client') as mock_create_client: mock_create_client.return_value = mock_client - common = self.driver._login() + self.driver._login() volume = self.volume.copy() model_update = self.driver.create_volume_from_snapshot( - self.volume, + volume, self.snapshot) - self.assertIsNone(model_update) + self.assertEqual(model_update, {}) comment = Comment({ "snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31", "display_name": "Foo Volume", "volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7", }) - volume_name_3par = common._encode_name(volume['id']) - osv_matcher = 'osv-' + volume_name_3par - omv_matcher = 'omv-' + volume_name_3par expected = [ mock.call.createSnapshot( @@ -3322,13 +3313,7 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver): 'oss-L4I73ONuTci9Fd4ceij-MQ', { 'comment': comment, - 'readOnly': False}), - mock.call.copyVolume( - osv_matcher, omv_matcher, HPE3PAR_CPG, mock.ANY), - mock.call.getTask(mock.ANY), - mock.call.getVolume(osv_matcher), - mock.call.deleteVolume(osv_matcher), - mock.call.modifyVolume(omv_matcher, {'newName': osv_matcher})] + 'readOnly': False})] mock_client.assert_has_calls( self.standard_login + @@ -3481,13 +3466,7 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver): def test_create_volume_from_snapshot_qos(self, _mock_volume_types): # setup_mock_client drive with default configuration # and return the mock HTTP 3PAR client - conf = { - 'getTask.return_value': { - 'status': 1}, - 'copyVolume.return_value': {'taskid': 1}, - 'getVolume.return_value': {} - } - mock_client = self.setup_driver(mock_conf=conf) + mock_client = self.setup_driver() _mock_volume_types.return_value = { 'name': 'gold', 'extra_specs': { @@ -3501,10 +3480,90 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver): with mock.patch.object(hpecommon.HPE3PARCommon, '_create_client') as mock_create_client: mock_create_client.return_value = mock_client - common = self.driver._login() + self.driver._login() volume = self.volume_qos.copy() model_update = self.driver.create_volume_from_snapshot( volume, self.snapshot) + self.assertEqual(model_update, {}) + + comment = Comment({ + "snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31", + "display_name": "Foo Volume", + "volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7", + }) + + expected = [ + mock.call.createSnapshot( + self.VOLUME_3PAR_NAME, + 'oss-L4I73ONuTci9Fd4ceij-MQ', { + 'comment': comment, + 'readOnly': False})] + + mock_client.assert_has_calls( + self.standard_login + + expected + + self.standard_logout) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_volume_from_snapshot_as_child(self, _mock_volume_types): + # setup_mock_client drive with default configuration + # and return the mock HTTP 3PAR client + mock_client = self.setup_driver() + volume_type_hos = self.volume_type_hos + volume_type_hos['extra_specs']['convert_to_base'] = False + _mock_volume_types.return_value = volume_type_hos + + with mock.patch.object(hpecommon.HPE3PARCommon, + '_create_client') as mock_create_client: + mock_create_client.return_value = mock_client + self.driver._login() + volume = self.volume_hos.copy() + model_update = self.driver.create_volume_from_snapshot( + volume, + self.snapshot) + self.assertEqual(model_update, {}) + + comment = Comment({ + "snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31", + "display_name": "Foo Volume", + "volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7", + }) + + expected = [ + mock.call.createSnapshot( + self.VOLUME_3PAR_NAME, + 'oss-L4I73ONuTci9Fd4ceij-MQ', + { + 'comment': comment, + 'readOnly': False})] + + mock_client.assert_has_calls( + self.standard_login + + expected + + self.standard_logout) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_volume_from_snapshot_as_base(self, _mock_volume_types): + # setup_mock_client drive with default configuration + # and return the mock HTTP 3PAR client + conf = { + 'getTask.return_value': { + 'status': 1}, + 'copyVolume.return_value': {'taskid': 1}, + 'getVolume.return_value': {} + } + mock_client = self.setup_driver(mock_conf=conf) + volume_type_hos = self.volume_type_hos + volume_type_hos['extra_specs']['convert_to_base'] = True + _mock_volume_types.return_value = volume_type_hos + with mock.patch.object(hpecommon.HPE3PARCommon, + '_create_client') as mock_create_client: + mock_create_client.return_value = mock_client + common = self.driver._login() + volume = self.volume_hos.copy() + model_update = self.driver.create_volume_from_snapshot( + volume, + self.snapshot) self.assertIsNone(model_update) comment = Comment({ @@ -3519,10 +3578,10 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver): expected = [ mock.call.createSnapshot( self.VOLUME_3PAR_NAME, - 'oss-L4I73ONuTci9Fd4ceij-MQ', { + 'oss-L4I73ONuTci9Fd4ceij-MQ', + { 'comment': comment, 'readOnly': False}), - mock.call.getCPG(HPE3PAR_CPG), mock.call.copyVolume( osv_matcher, omv_matcher, HPE3PAR_CPG, mock.ANY), mock.call.getTask(mock.ANY), @@ -3535,6 +3594,116 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver): expected + self.standard_logout) + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_volume_from_snapshot_as_child_and_extend( + self, _mock_volume_types): + # setup_mock_client drive with default configuration + # and return the mock HTTP 3PAR client + conf = { + 'getTask.return_value': { + 'status': 1}, + 'copyVolume.return_value': {'taskid': 1}, + 'getVolume.return_value': {} + } + mock_client = self.setup_driver(mock_conf=conf) + volume_type_hos = self.volume_type_hos + volume_type_hos['extra_specs']['convert_to_base'] = False + _mock_volume_types.return_value = volume_type_hos + with mock.patch.object(hpecommon.HPE3PARCommon, + '_create_client') as mock_create_client: + mock_create_client.return_value = mock_client + common = self.driver._login() + volume = self.volume_hos.copy() + volume['size'] = self.volume['size'] + 10 + model_update = self.driver.create_volume_from_snapshot( + volume, + self.snapshot) + self.assertIsNone(model_update) + + comment = Comment({ + "snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31", + "display_name": "Foo Volume", + "volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7", + }) + volume_name_3par = common._encode_name(volume['id']) + osv_matcher = 'osv-' + volume_name_3par + omv_matcher = 'omv-' + volume_name_3par + + expected = [ + mock.call.createSnapshot( + self.VOLUME_3PAR_NAME, + 'oss-L4I73ONuTci9Fd4ceij-MQ', + { + 'comment': comment, + 'readOnly': False}), + mock.call.copyVolume( + osv_matcher, omv_matcher, HPE3PAR_CPG, mock.ANY), + mock.call.getTask(mock.ANY), + mock.call.getVolume(osv_matcher), + mock.call.deleteVolume(osv_matcher), + mock.call.modifyVolume(omv_matcher, {'newName': osv_matcher}), + mock.call.growVolume(osv_matcher, 10 * 1024)] + + mock_client.assert_has_calls( + self.standard_login + + expected + + self.standard_logout) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_volume_from_snapshot_as_base_and_extend( + self, _mock_volume_types): + # setup_mock_client drive with default configuration + # and return the mock HTTP 3PAR client + conf = { + 'getTask.return_value': { + 'status': 1}, + 'copyVolume.return_value': {'taskid': 1}, + 'getVolume.return_value': {} + } + mock_client = self.setup_driver(mock_conf=conf) + volume_type_hos = self.volume_type_hos + volume_type_hos['extra_specs']['convert_to_base'] = True + _mock_volume_types.return_value = volume_type_hos + with mock.patch.object(hpecommon.HPE3PARCommon, + '_create_client') as mock_create_client: + mock_create_client.return_value = mock_client + common = self.driver._login() + volume = self.volume_hos.copy() + volume['size'] = self.volume['size'] + 10 + model_update = self.driver.create_volume_from_snapshot( + volume, + self.snapshot) + self.assertIsNone(model_update) + + comment = Comment({ + "snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31", + "display_name": "Foo Volume", + "volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7", + }) + volume_name_3par = common._encode_name(volume['id']) + osv_matcher = 'osv-' + volume_name_3par + omv_matcher = 'omv-' + volume_name_3par + + expected = [ + mock.call.createSnapshot( + self.VOLUME_3PAR_NAME, + 'oss-L4I73ONuTci9Fd4ceij-MQ', + { + 'comment': comment, + 'readOnly': False}), + mock.call.copyVolume( + osv_matcher, omv_matcher, HPE3PAR_CPG, mock.ANY), + mock.call.getTask(mock.ANY), + mock.call.getVolume(osv_matcher), + mock.call.deleteVolume(osv_matcher), + mock.call.modifyVolume(omv_matcher, {'newName': osv_matcher}), + mock.call.growVolume(osv_matcher, 10 * 1024)] + + mock_client.assert_has_calls( + self.standard_login + + expected + + self.standard_logout) + def test_terminate_connection_from_primary_when_failed_over(self): # setup_mock_client drive with default configuration # and return the mock HTTP 3PAR client diff --git a/cinder/volume/drivers/hpe/hpe_3par_common.py b/cinder/volume/drivers/hpe/hpe_3par_common.py index d6442a503d1..23bd5f1c40b 100644 --- a/cinder/volume/drivers/hpe/hpe_3par_common.py +++ b/cinder/volume/drivers/hpe/hpe_3par_common.py @@ -273,11 +273,12 @@ class HPE3PARCommon(object): 4.0.9 - Set proper backend on subsequent operation, after group failover. bug #1773069 4.0.10 - Added retry in delete_volume. bug #1783934 + 4.0.11 - Added extra spec hpe3par:convert_to_base """ - VERSION = "4.0.10" + VERSION = "4.0.11" stats = {} @@ -331,7 +332,8 @@ class HPE3PARCommon(object): 'priority'] qos_priority_level = {'low': 1, 'normal': 2, 'high': 3} hpe3par_valid_keys = ['cpg', 'snap_cpg', 'provisioning', 'persona', 'vvs', - 'flash_cache', 'compression', 'group_replication'] + 'flash_cache', 'compression', 'group_replication', + 'convert_to_base'] def __init__(self, config, active_backend_id=None): self.config = config @@ -1797,6 +1799,16 @@ class HPE3PARCommon(object): else: return default + def _get_boolean_key_value(self, hpe3par_keys, key, default=False): + value = self._get_key_value( + hpe3par_keys, key, default) + if isinstance(value, six.string_types): + if value.lower() == 'true': + value = True + else: + value = False + return value + def _get_qos_value(self, qos, key, default=None): if key in qos: return qos[key] @@ -2086,6 +2098,10 @@ class HPE3PARCommon(object): hpe3par_tiramisu = ( self._get_key_value(hpe3par_keys, 'group_replication')) + # by default, set convert_to_base to False + convert_to_base = self._get_boolean_key_value( + hpe3par_keys, 'convert_to_base', False) + # if provisioning is not set use thin default_prov = self.valid_prov_values[0] prov_value = self._get_key_value(hpe3par_keys, 'provisioning', @@ -2121,7 +2137,8 @@ class HPE3PARCommon(object): 'vvs_name': vvs_name, 'qos': qos, 'tpvv': tpvv, 'tdvv': tdvv, 'volume_type': volume_type, - 'group_replication': hpe3par_tiramisu} + 'group_replication': hpe3par_tiramisu, + 'convert_to_base': convert_to_base} def get_volume_settings_from_type(self, volume, host=None): """Get 3PAR volume settings given a volume. @@ -2624,13 +2641,23 @@ class HPE3PARCommon(object): self.client.createSnapshot(volume_name, snap_name, optional) - # Convert snapshot volume to base volume type - LOG.debug('Converting to base volume type: %s.', - volume['id']) - model_update = self._convert_to_base_volume(volume) + # by default, set convert_to_base to False + convert_to_base = self._get_boolean_key_value( + hpe3par_keys, 'convert_to_base', False) + + LOG.debug("convert_to_base: %(convert)s", + {'convert': convert_to_base}) - # Grow the snapshot if needed growth_size = volume['size'] - snapshot['volume_size'] + LOG.debug("growth_size: %(size)s", {'size': growth_size}) + if growth_size > 0 or convert_to_base: + # Convert snapshot volume to base volume type + LOG.debug('Converting to base volume type: %(id)s.', + {'id': volume['id']}) + model_update = self._convert_to_base_volume(volume) + else: + LOG.debug("volume is created as child of snapshot") + if growth_size > 0: try: growth_size_mib = growth_size * units.Gi / units.Mi @@ -2926,6 +2953,37 @@ class HPE3PARCommon(object): "can't be deleted at this time.") raise exception.SnapshotIsBusy(message=msg) + if snap.startswith('osv-'): + LOG.info( + "Found a volume %(name)s", + {'name': snap}) + + # Get details of original volume v1 + # These details would be required to form v2 + s1_detail = self.client.getVolume(snap_name) + v1_name = s1_detail.get('copyOf') + v1 = self.client.getVolume(v1_name) + + # Get details of volume v2, + # which is child of snapshot s1 + v2_name = snap + v2 = self.client.getVolume(v2_name) + + # Update v2 object as required for + # _convert_to_base function + v2['volume_type_id'] = \ + self._get_3par_vol_comment_value( + v1['comment'], 'volume_type_id') + + v2['id'] = self._get_3par_vol_comment_value( + v2['comment'], 'volume_id') + + v2['host'] = '#' + v1['userCPG'] + + LOG.debug('Converting to base volume type: ' + '%(id)s.', {'id': v2['id']}) + self._convert_to_base_volume(v2) + try: self.client.deleteVolume(snap_name) except Exception: