From e458bdbf84aba2dab5fc0f65a49764466016558b Mon Sep 17 00:00:00 2001 From: Ryan Liang Date: Wed, 4 Jul 2018 13:56:46 +0800 Subject: [PATCH] Unity: add thick volume support Change-Id: Ic1a77f928ce0b8a51e78b33bc6025b7c9902c8fa --- .../drivers/dell_emc/unity/fake_exception.py | 8 +++ .../drivers/dell_emc/unity/test_adapter.py | 53 ++++++++++++++++--- .../drivers/dell_emc/unity/test_client.py | 13 ++++- .../volume/drivers/dell_emc/unity/adapter.py | 44 +++++++++++---- .../volume/drivers/dell_emc/unity/client.py | 6 ++- .../drivers/dell-emc-unity-driver.rst | 24 ++++++--- driver-requirements.txt | 4 +- .../unity-thick-support-fdbef833f2b4d54f.yaml | 7 +++ 8 files changed, 129 insertions(+), 30 deletions(-) create mode 100644 releasenotes/notes/unity-thick-support-fdbef833f2b4d54f.yaml diff --git a/cinder/tests/unit/volume/drivers/dell_emc/unity/fake_exception.py b/cinder/tests/unit/volume/drivers/dell_emc/unity/fake_exception.py index cdcd40230fe..81db860e245 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/unity/fake_exception.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/unity/fake_exception.py @@ -84,3 +84,11 @@ class AdapterSetupError(Exception): class HostDeleteIsCalled(Exception): pass + + +class UnityThinCloneNotAllowedError(StoropsException): + pass + + +class SystemAPINotSupported(StoropsException): + pass diff --git a/cinder/tests/unit/volume/drivers/dell_emc/unity/test_adapter.py b/cinder/tests/unit/volume/drivers/dell_emc/unity/test_adapter.py index b2b487e9e73..6a7b0eeb7bd 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/unity/test_adapter.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/unity/test_adapter.py @@ -78,8 +78,13 @@ class MockClient(object): return test_client.MockResourceList(['pool0', 'pool1']) @staticmethod - def create_lun(name, size, pool, description=None, io_limit_policy=None): - return test_client.MockResource(_id=name, name=name) + def create_lun(name, size, pool, description=None, io_limit_policy=None, + is_thin=None): + lun_id = name + if is_thin is not None and not is_thin: + lun_id += '_thick' + + return test_client.MockResource(_id=lun_id, name=name) @staticmethod def get_lun(name=None, lun_id=None): @@ -198,6 +203,8 @@ class MockClient(object): if (obj.name, name) in ( ('snap_61', 'lun_60'), ('lun_63', 'lun_60')): return test_client.MockResource(_id=name) + elif (obj.name, name) in (('snap_71', 'lun_70'), ('lun_72', 'lun_70')): + raise ex.UnityThinCloneNotAllowedError() else: raise ex.UnityThinCloneLimitExceededError @@ -237,8 +244,7 @@ def mock_adapter(driver_clz): ret = driver_clz() ret._client = MockClient() with mock.patch('cinder.volume.drivers.dell_emc.unity.adapter.' - 'CommonAdapter.validate_ports'), \ - patch_storops(): + 'CommonAdapter.validate_ports'), patch_storops(): ret.do_setup(MockDriver(), MockConfig()) ret.lookup_service = MockLookupService() return ret @@ -272,8 +278,17 @@ def get_connection_info(adapter, hlu, host, connector): return {} +def get_volume_type_extra_specs(type_id): + if type_id == 'thick': + return {'provisioning:type': 'thick', + 'thick_provisioning_support': ' True'} + return {} + + def patch_for_unity_adapter(func): @functools.wraps(func) + @mock.patch('cinder.volume.volume_types.get_volume_type_extra_specs', + new=get_volume_type_extra_specs) @mock.patch('cinder.volume.drivers.dell_emc.unity.utils.' 'get_backend_qos_specs', new=get_backend_qos_specs) @@ -294,6 +309,7 @@ def patch_for_concrete_adapter(clz_str): new=get_connection_info) def func_wrapper(*args, **kwargs): return func(*args, **kwargs) + return func_wrapper return inner_decorator @@ -302,7 +318,6 @@ def patch_for_concrete_adapter(clz_str): patch_for_iscsi_adapter = patch_for_concrete_adapter( 'cinder.volume.drivers.dell_emc.unity.adapter.ISCSIAdapter') - patch_for_fc_adapter = patch_for_concrete_adapter( 'cinder.volume.drivers.dell_emc.unity.adapter.FCAdapter') @@ -367,6 +382,15 @@ class CommonAdapterTest(test.TestCase): expected = get_lun_pl('lun_3') self.assertEqual(expected, ret['provider_location']) + @patch_for_unity_adapter + def test_create_volume_thick(self): + volume = MockOSResource(name='lun_3', size=5, host='unity#pool1', + volume_type_id='thick') + ret = self.adapter.create_volume(volume) + + expected = get_lun_pl('lun_3_thick') + self.assertEqual(expected, ret['provider_location']) + def test_create_snapshot(self): volume = MockOSResource(provider_location='id^lun_43') snap = MockOSResource(volume=volume, name='abc-def_snap') @@ -405,7 +429,7 @@ class CommonAdapterTest(test.TestCase): self.assertEqual(2, stats['free_capacity_gb']) self.assertEqual(300, stats['max_over_subscription_ratio']) self.assertEqual(5, stats['reserved_percentage']) - self.assertFalse(stats['thick_provisioning_support']) + self.assertTrue(stats['thick_provisioning_support']) self.assertTrue(stats['thin_provisioning_support']) def test_update_volume_stats(self): @@ -413,7 +437,7 @@ class CommonAdapterTest(test.TestCase): self.assertEqual('backend', stats['volume_backend_name']) self.assertEqual('unknown', stats['storage_protocol']) self.assertTrue(stats['thin_provisioning_support']) - self.assertFalse(stats['thick_provisioning_support']) + self.assertTrue(stats['thick_provisioning_support']) self.assertEqual(1, len(stats['pools'])) def test_serial_number(self): @@ -432,6 +456,7 @@ class CommonAdapterTest(test.TestCase): 'CommonAdapter.validate_ports'): self.adapter._client.system.system_version = '4.0.0' self.adapter.do_setup(self.adapter.driver, MockConfig()) + self.assertRaises(exception.VolumeBackendAPIException, f) def test_verify_cert_false_path_none(self): @@ -688,6 +713,20 @@ class CommonAdapterTest(test.TestCase): new_dd_lun) self.assertEqual(IdMatcher(test_client.MockResource(_id=lun_id)), ret) + @patch_for_unity_adapter + def test_thin_clone_thick(self): + lun_id = 'lun_70' + src_snap_id = 'snap_71' + volume = MockOSResource(name=lun_id, id=lun_id, size=1, + provider_location=get_snap_lun_pl(lun_id)) + src_snap = test_client.MockResource(name=src_snap_id, _id=src_snap_id) + new_dd_lun = test_client.MockResource(name='lun_73') + with patch_storops(), patch_dd_copy(new_dd_lun) as dd: + vol_params = adapter.VolumeParams(self.adapter, volume) + ret = self.adapter._thin_clone(vol_params, src_snap) + dd.assert_called_with(vol_params, src_snap, src_lun=None) + self.assertEqual(ret, new_dd_lun) + def test_extend_volume_error(self): def f(): volume = MockOSResource(id='l56', diff --git a/cinder/tests/unit/volume/drivers/dell_emc/unity/test_client.py b/cinder/tests/unit/volume/drivers/dell_emc/unity/test_client.py index 9a444dd5fc4..fb06b61bb94 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/unity/test_client.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/unity/test_client.py @@ -48,6 +48,7 @@ class MockResource(object): self.pool_name = 'Pool0' self._storage_resource = None self.host_cache = [] + self.is_thin = None @property def id(self): @@ -111,13 +112,16 @@ class MockResource(object): return self.alu_hlu_map.get(lun.get_id(), None) @staticmethod - def create_lun(lun_name, size_gb, description=None, io_limit_policy=None): + def create_lun(lun_name, size_gb, description=None, io_limit_policy=None, + is_thin=None): if lun_name == 'in_use': raise ex.UnityLunNameInUseError() ret = MockResource(lun_name, 'lun_2') if io_limit_policy is not None: ret.max_iops = io_limit_policy.max_iops ret.max_kbps = io_limit_policy.max_kbps + if is_thin is not None: + ret.is_thin = is_thin return ret @staticmethod @@ -340,6 +344,13 @@ class ClientTest(unittest.TestCase): lun = self.client.create_lun('LUN 4', 6, pool, io_limit_policy=limit) self.assertEqual(100, lun.max_kbps) + def test_create_lun_thick(self): + name = 'thick_lun' + pool = MockResource('Pool 0') + lun = self.client.create_lun(name, 6, pool, is_thin=False) + self.assertIsNotNone(lun.is_thin) + self.assertFalse(lun.is_thin) + def test_thin_clone_success(self): name = 'tc_77' src_lun = MockResource(_id='id_77') diff --git a/cinder/volume/drivers/dell_emc/unity/adapter.py b/cinder/volume/drivers/dell_emc/unity/adapter.py index 45ed0fef6bf..b50b9d5f31b 100644 --- a/cinder/volume/drivers/dell_emc/unity/adapter.py +++ b/cinder/volume/drivers/dell_emc/unity/adapter.py @@ -38,7 +38,6 @@ else: # Set storops_ex to be None for unit test storops_ex = None - LOG = logging.getLogger(__name__) PROTOCOL_FC = 'FC' @@ -58,6 +57,7 @@ class VolumeParams(object): else volume.display_name) self._pool = None self._io_limit_policy = None + self._is_thick = None @property def volume_id(self): @@ -109,11 +109,21 @@ class VolumeParams(object): def io_limit_policy(self, value): self._io_limit_policy = value + @property + def is_thick(self): + if self._is_thick is None: + provision = utils.get_extra_spec(self._volume, 'provisioning:type') + support = utils.get_extra_spec(self._volume, + 'thick_provisioning_support') + self._is_thick = (provision == 'thick' and support == ' True') + return self._is_thick + def __eq__(self, other): return (self.volume_id == other.volume_id and self.name == other.name and self.size == other.size and - self.io_limit_policy == other.io_limit_policy) + self.io_limit_policy == other.io_limit_policy and + self.is_thick == other.is_thick) class CommonAdapter(object): @@ -149,8 +159,8 @@ class CommonAdapter(object): self.reserved_percentage = self.config.reserved_percentage self.max_over_subscription_ratio = ( self.config.max_over_subscription_ratio) - self.volume_backend_name = ( - self.config.safe_get('volume_backend_name') or self.driver_name) + self.volume_backend_name = (self.config.safe_get('volume_backend_name') + or self.driver_name) self.ip = self.config.san_ip self.username = self.config.san_login self.password = self.config.san_password @@ -274,18 +284,21 @@ class CommonAdapter(object): 'size': params.size, 'description': params.description, 'pool': params.pool, - 'io_limit_policy': params.io_limit_policy} + 'io_limit_policy': params.io_limit_policy, + 'is_thick': params.is_thick + } LOG.info('Create Volume: %(name)s, size: %(size)s, description: ' '%(description)s, pool: %(pool)s, io limit policy: ' - '%(io_limit_policy)s.', log_params) + '%(io_limit_policy)s, thick: %(is_thick)s.', log_params) return self.makeup_model( self.client.create_lun(name=params.name, size=params.size, pool=params.pool, description=params.description, - io_limit_policy=params.io_limit_policy)) + io_limit_policy=params.io_limit_policy, + is_thin=False if params.is_thick else None)) def delete_volume(self, volume): lun_id = self.get_lun_id(volume) @@ -404,7 +417,7 @@ class CommonAdapter(object): 'volume_backend_name': self.volume_backend_name, 'storage_protocol': self.protocol, 'thin_provisioning_support': True, - 'thick_provisioning_support': False, + 'thick_provisioning_support': True, 'pools': self.get_pools_stats(), } @@ -428,7 +441,7 @@ class CommonAdapter(object): {'pool_name': pool.name, 'array_serial': self.serial_number}), 'thin_provisioning_support': True, - 'thick_provisioning_support': False, + 'thick_provisioning_support': True, 'max_over_subscription_ratio': ( self.max_over_subscription_ratio)} @@ -583,7 +596,8 @@ class CommonAdapter(object): dest_lun = self.client.create_lun( name=vol_params.name, size=vol_params.size, pool=vol_params.pool, description=vol_params.description, - io_limit_policy=vol_params.io_limit_policy) + io_limit_policy=vol_params.io_limit_policy, + is_thin=False if vol_params.is_thick else None) src_id = src_snap.get_id() try: conn_props = cinder_utils.brick_get_connector_properties() @@ -653,6 +667,16 @@ class CommonAdapter(object): 'thin clone api. source snap: %(src_snap)s, lun: %(src_lun)s.', {'src_snap': src_snap.name, 'src_lun': 'Unknown' if src_lun is None else src_lun.name}) + except storops_ex.UnityThinCloneNotAllowedError: + # Thin clone not allowed on some resources, + # like thick luns and their snaps + lun = self._dd_copy(vol_params, src_snap, src_lun=src_lun) + LOG.debug( + 'Volume copied via dd because source snap/lun is not allowed ' + 'to thin clone, i.e. it is thick. source snap: %(src_snap)s, ' + 'lun: %(src_lun)s.', + {'src_snap': src_snap.name, + 'src_lun': 'Unknown' if src_lun is None else src_lun.name}) return lun def create_volume_from_snapshot(self, volume, snapshot): diff --git a/cinder/volume/drivers/dell_emc/unity/client.py b/cinder/volume/drivers/dell_emc/unity/client.py index fcd8783995b..21e3f087d1d 100644 --- a/cinder/volume/drivers/dell_emc/unity/client.py +++ b/cinder/volume/drivers/dell_emc/unity/client.py @@ -57,7 +57,7 @@ class UnityClient(object): return self.system.serial_number def create_lun(self, name, size, pool, description=None, - io_limit_policy=None): + io_limit_policy=None, is_thin=None): """Creates LUN on the Unity system. :param name: lun name @@ -65,12 +65,14 @@ class UnityClient(object): :param pool: UnityPool object represent to pool to place the lun :param description: lun description :param io_limit_policy: io limit on the LUN + :param is_thin: if False, a thick LUN will be created :return: UnityLun object """ try: lun = pool.create_lun(lun_name=name, size_gb=size, description=description, - io_limit_policy=io_limit_policy) + io_limit_policy=io_limit_policy, + is_thin=is_thin) except storops_ex.UnityLunNameInUseError: LOG.debug("LUN %s already exists. Return the existing one.", name) diff --git a/doc/source/configuration/block-storage/drivers/dell-emc-unity-driver.rst b/doc/source/configuration/block-storage/drivers/dell-emc-unity-driver.rst index 999989df11a..a0d65e1322a 100644 --- a/doc/source/configuration/block-storage/drivers/dell-emc-unity-driver.rst +++ b/doc/source/configuration/block-storage/drivers/dell-emc-unity-driver.rst @@ -10,13 +10,13 @@ and a Dell EMC distributed Python package Prerequisites ~~~~~~~~~~~~~ -+-------------------+----------------+ -| Software | Version | -+===================+================+ -| Unity OE | 4.1.X or newer | -+-------------------+----------------+ -| storops | 0.5.7 or newer | -+-------------------+----------------+ ++-------------------+-----------------+ +| Software | Version | ++===================+=================+ +| Unity OE | 4.1.X or newer | ++-------------------+-----------------+ +| storops | 0.5.10 or newer | ++-------------------+-----------------+ Supported operations @@ -33,6 +33,7 @@ Supported operations - Get volume statistics. - Efficient non-disruptive volume backup. - Revert a volume to a snapshot. +- Create thick volumes. Driver configuration ~~~~~~~~~~~~~~~~~~~~ @@ -237,7 +238,14 @@ To enable multipath in live migration: Thin and thick provisioning ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Only thin volume provisioning is supported in Unity volume driver. +By default, the volume created by Unity driver is thin provisioned. Run the +following commands to create a thick volume. + +.. code-block:: console + + # openstack volume type create --property provisioning:type=thick \ + --property thick_provisioning_support=' True' thick_volume_type + # openstack volume create --type thick_volume_type thick_volume QoS support diff --git a/driver-requirements.txt b/driver-requirements.txt index 0c7fdcc27a5..8adca4296b9 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -31,8 +31,8 @@ pyxcli>=1.1.5 # Apache-2.0 rados # LGPLv2.1 rbd # LGPLv2.1 -# Dell EMC VNX -storops>=0.5.7 # Apache-2.0 +# Dell EMC VNX and Unity +storops>=0.5.10 # Apache-2.0 # Violin vmemclient>=1.1.8 # Apache-2.0 diff --git a/releasenotes/notes/unity-thick-support-fdbef833f2b4d54f.yaml b/releasenotes/notes/unity-thick-support-fdbef833f2b4d54f.yaml new file mode 100644 index 00000000000..5485bd7daed --- /dev/null +++ b/releasenotes/notes/unity-thick-support-fdbef833f2b4d54f.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Dell EMC Unity Driver: Add thick volume support. Refer to `Unity Cinder + Configuration document + `__ + to create a thick volume.