From 6ccfcafd45c445bc48593a310a3649e18c8b8a51 Mon Sep 17 00:00:00 2001 From: Peter Wang Date: Fri, 26 Aug 2016 09:57:58 +0800 Subject: [PATCH] VNX: Add async migration support VNX cinder driver now supports async migration during volume cloning. By default, User can create cloned volume after the migration starts in the VNX instead of waiting for the completion of migration. this greatly accelerates the cloning process. If user wants to disable this, he could add '--metadata async_migrate=False' when creating volume from source volume/snapshot. DocImpact Closes-bug: #1657966 Change-Id: Idfe5d9d4043f84cad2ae2d4d31914d3ae573de50 --- .../drivers/dell_emc/vnx/fake_exception.py | 8 +++ .../__init__.py} | 0 .../dell_emc/vnx/fake_storops/lib/__init__.py | 0 .../dell_emc/vnx/fake_storops/lib/tasks.py | 28 +++++++++ .../drivers/dell_emc/vnx/mocked_cinder.yaml | 5 ++ .../drivers/dell_emc/vnx/mocked_vnx.yaml | 54 ++++++++++++++++ .../volume/drivers/dell_emc/vnx/res_mock.py | 3 +- .../drivers/dell_emc/vnx/test_adapter.py | 32 +++++++++- .../drivers/dell_emc/vnx/test_client.py | 13 +++- .../drivers/dell_emc/vnx/test_taskflows.py | 4 +- cinder/volume/drivers/dell_emc/vnx/adapter.py | 52 ++++++++++++--- cinder/volume/drivers/dell_emc/vnx/client.py | 52 +++++++++++---- cinder/volume/drivers/dell_emc/vnx/common.py | 5 +- cinder/volume/drivers/dell_emc/vnx/driver.py | 3 +- .../volume/drivers/dell_emc/vnx/taskflows.py | 63 ++++++++++++------- cinder/volume/drivers/dell_emc/vnx/utils.py | 18 +++++- ...nc-migration-support-3c449139bb264004.yaml | 10 +++ 17 files changed, 298 insertions(+), 52 deletions(-) rename cinder/tests/unit/volume/drivers/dell_emc/vnx/{fake_storops.py => fake_storops/__init__.py} (100%) create mode 100644 cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_storops/lib/__init__.py create mode 100644 cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_storops/lib/tasks.py create mode 100644 releasenotes/notes/vnx-async-migration-support-3c449139bb264004.yaml diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_exception.py b/cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_exception.py index dbddd8dc7bf..c5ce0c52914 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_exception.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_exception.py @@ -60,6 +60,10 @@ class VNXMigrationError(VNXException): pass +class VNXLunNotMigratingError(VNXException): + pass + + class VNXTargetNotReadyError(VNXMigrationError): message = 'The destination LUN is not available for migration' @@ -128,6 +132,10 @@ class VNXDeleteLunError(VNXLunError): pass +class VNXLunUsedByFeatureError(VNXLunError): + pass + + class VNXCompressionError(VNXLunError): pass diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_storops.py b/cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_storops/__init__.py similarity index 100% rename from cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_storops.py rename to cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_storops/__init__.py diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_storops/lib/__init__.py b/cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_storops/lib/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_storops/lib/tasks.py b/cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_storops/lib/tasks.py new file mode 100644 index 00000000000..04375297dc8 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/dell_emc/vnx/fake_storops/lib/tasks.py @@ -0,0 +1,28 @@ +# Copyright (c) 2017 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +class PQueue(object): + + def __init__(self, path, interval=None): + self.path = path + self._interval = interval + self.started = False + + def put(self, item): + return item + + def start(self): + self.started = True diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_cinder.yaml b/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_cinder.yaml index ae781f94445..2c800875f25 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_cinder.yaml +++ b/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_cinder.yaml @@ -94,6 +94,11 @@ test_delete_volume_not_force: &test_delete_volume_not_force test_delete_volume_force: *test_delete_volume_not_force +test_delete_async_volume: + volume: *volume_base + +test_delete_async_volume_migrating: + volume: *volume_base test_retype_need_migration_when_host_changed: volume: *volume_base diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_vnx.yaml b/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_vnx.yaml index 2bac48eaba7..01e6e273ba0 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_vnx.yaml +++ b/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_vnx.yaml @@ -377,6 +377,17 @@ test_cleanup_migration: get_migration_session: *session_cancel get_lun: *lun_cancel_migrate +test_cleanup_migration_not_migrating: + lun: &lun_cancel_migrate_not_migrating + _methods: + cancel_migrate: + _raise: + VNXLunNotMigratingError: The LUN is not migrating + vnx: + _methods: + get_migration_session: *session_cancel + get_lun: *lun_cancel_migrate_not_migrating + test_get_lun_by_name: lun: &lun_test_get_lun_by_name _properties: @@ -463,6 +474,26 @@ test_delete_lun_exception: _methods: get_lun: *lun_test_delete_lun_exception +test_cleanup_async_lun: + lun: &lun_test_cleanup_async_lun + _properties: + <<: *lun_base_prop + name: lun_test_cleanup_async_lun + is_snap_mount_point: True + _methods: + delete: + cancel_migrate: + snap: &snap_test_cleanup_async_lun + _methods: + delete: + vnx: + _properties: + <<: *vnx_base_prop + _methods: + get_lun: *lun_test_cleanup_async_lun + get_snap: *snap_test_cleanup_async_lun + get_migration_session: *session_migrating + test_create_cg: &test_create_cg cg: &cg_for_create _properties: @@ -1211,6 +1242,29 @@ test_append_volume_stats: test_delete_volume_not_force: *test_delete_lun test_delete_volume_force: *test_delete_lun +test_delete_async_volume: + snap: &snap_test_delete_async_volume + _methods: + delete: + vnx: + _methods: + get_lun: *lun_test_delete_lun + get_snap: *snap_test_delete_async_volume + +test_delete_async_volume_migrating: + lun: &lun_used_by_feature + _properties: + is_snap_mount_point: false + _methods: + cancel_migrate: + delete: + _raise: + VNXLunUsedByFeatureError: + vnx: + _methods: + get_lun: *lun_used_by_feature + get_snap: *snap_test_delete_async_volume + test_enable_compression: lun: _properties: diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vnx/res_mock.py b/cinder/tests/unit/volume/drivers/dell_emc/vnx/res_mock.py index a56b68af204..e68fd054c11 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/vnx/res_mock.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/vnx/res_mock.py @@ -363,7 +363,8 @@ def _build_client(): password='sysadmin', scope='global', naviseccli=None, - sec_file=None) + sec_file=None, + queue_path='vnx-cinder') def patch_client(func): diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_adapter.py b/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_adapter.py index 54f6da52d7d..ec2bc5b400a 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_adapter.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_adapter.py @@ -120,6 +120,7 @@ class TestCommonAdapter(test.TestCase): def test_create_volume_from_snapshot( self, vnx_common, mocked, cinder_input): volume = cinder_input['volume'] + volume['metadata'] = {'async_migrate': 'False'} snapshot = cinder_input['snapshot'] snapshot.volume = volume update = vnx_common.create_volume_from_snapshot(volume, snapshot) @@ -293,7 +294,9 @@ class TestCommonAdapter(test.TestCase): @res_mock.patch_common_adapter def test_delete_volume_not_force(self, vnx_common, mocked, mocked_input): vnx_common.force_delete_lun_in_sg = False - vnx_common.delete_volume(mocked_input['volume']) + volume = mocked_input['volume'] + volume['metadata'] = {'async_migrate': 'False'} + vnx_common.delete_volume(volume) lun = vnx_common.client.vnx.get_lun() lun.delete.assert_called_with(force_detach=True, detach_from_sg=False) @@ -301,7 +304,32 @@ class TestCommonAdapter(test.TestCase): @res_mock.patch_common_adapter def test_delete_volume_force(self, vnx_common, mocked, mocked_input): vnx_common.force_delete_lun_in_sg = True - vnx_common.delete_volume(mocked_input['volume']) + volume = mocked_input['volume'] + volume['metadata'] = {'async_migrate': 'False'} + vnx_common.delete_volume(volume) + lun = vnx_common.client.vnx.get_lun() + lun.delete.assert_called_with(force_detach=True, detach_from_sg=True) + + @res_mock.mock_driver_input + @res_mock.patch_common_adapter + def test_delete_async_volume(self, vnx_common, mocked, mocked_input): + volume = mocked_input['volume'] + volume.metadata = {'async_migrate': 'True'} + vnx_common.force_delete_lun_in_sg = True + vnx_common.delete_volume(volume) + lun = vnx_common.client.vnx.get_lun() + lun.delete.assert_called_with(force_detach=True, detach_from_sg=True) + + @res_mock.mock_driver_input + @res_mock.patch_common_adapter + def test_delete_async_volume_migrating(self, vnx_common, mocked, + mocked_input): + + volume = mocked_input['volume'] + volume.metadata = {'async_migrate': 'True'} + vnx_common.force_delete_lun_in_sg = True + vnx_common.client.cleanup_async_lun = mock.Mock() + vnx_common.delete_volume(volume) lun = vnx_common.client.vnx.get_lun() lun.delete.assert_called_with(force_detach=True, detach_from_sg=True) diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_client.py b/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_client.py index 689845ea55b..c2387eb613d 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_client.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_client.py @@ -159,6 +159,10 @@ class TestClient(test.TestCase): def test_cleanup_migration(self, client, mocked): client.cleanup_migration(1, 2) + @res_mock.patch_client + def test_cleanup_migration_not_migrating(self, client, mocked): + client.cleanup_migration(1, 2) + @res_mock.patch_client def test_get_lun_by_name(self, client, mocked): lun = client.get_lun(name='lun_name_test_get_lun_by_name') @@ -182,6 +186,12 @@ class TestClient(test.TestCase): 'General lun delete error.', client.delete_lun, mocked['lun'].name) + @res_mock.patch_client + def test_cleanup_async_lun(self, client, mocked): + client.cleanup_async_lun( + mocked['lun'].name, + force=True) + @res_mock.patch_client def test_enable_compression(self, client, mocked): lun_obj = mocked['lun'] @@ -261,7 +271,8 @@ class TestClient(test.TestCase): lun = client.vnx.get_lun() lun.create_snap.assert_called_once_with('snap_test_create_snapshot', allow_rw=True, - auto_delete=False) + auto_delete=False, + keep_for=None) @res_mock.patch_client def test_create_snapshot_snap_name_exist_error(self, client, _ignore): diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_taskflows.py b/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_taskflows.py index dfc472087c4..a394cd9eb07 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_taskflows.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_taskflows.py @@ -137,7 +137,7 @@ class TestTaskflow(test.TestCase): 'client': client, 'snap_name': 'snap_name' } - self.work_flow.add(vnx_taskflow.AllowReadWriteTask()) + self.work_flow.add(vnx_taskflow.ModifySnapshotTask()) engine = taskflow.engines.load(self.work_flow, store=store_spec) engine.run() @@ -148,7 +148,7 @@ class TestTaskflow(test.TestCase): 'client': client, 'snap_name': 'snap_name' } - self.work_flow.add(vnx_taskflow.AllowReadWriteTask()) + self.work_flow.add(vnx_taskflow.ModifySnapshotTask()) engine = taskflow.engines.load(self.work_flow, store=store_spec) self.assertRaises(vnx_ex.VNXSnapError, diff --git a/cinder/volume/drivers/dell_emc/vnx/adapter.py b/cinder/volume/drivers/dell_emc/vnx/adapter.py index bb81ef5f3f2..3e02e137e4f 100644 --- a/cinder/volume/drivers/dell_emc/vnx/adapter.py +++ b/cinder/volume/drivers/dell_emc/vnx/adapter.py @@ -59,6 +59,7 @@ class CommonAdapter(object): self.reserved_percentage = None self.destroy_empty_sg = None self.itor_auto_dereg = None + self.queue_path = None def do_setup(self): self._normalize_config() @@ -68,7 +69,8 @@ class CommonAdapter(object): self.config.san_password, self.config.storage_vnx_authentication_type, self.config.naviseccli_path, - self.config.storage_vnx_security_file_dir) + self.config.storage_vnx_security_file_dir, + self.queue_path) # Replication related self.mirror_view = self.build_mirror_view(self.config, True) self.serial_number = self.client.get_serial() @@ -86,6 +88,9 @@ class CommonAdapter(object): self.set_extra_spec_defaults() def _normalize_config(self): + self.queue_path = ( + self.config.config_group if self.config.config_group + else 'DEFAULT') # Check option `naviseccli_path`. # Set to None (then pass to storops) if it is not set or set to an # empty string. @@ -301,7 +306,7 @@ class CommonAdapter(object): tier = specs.tier base_lun_name = utils.get_base_lun_name(snapshot.volume) rep_update = dict() - if utils.snapcopy_enabled(volume): + if utils.is_snapcopy_enabled(volume): new_lun_id = emc_taskflow.fast_create_volume_from_snapshot( client=self.client, snap_name=snapshot.name, @@ -309,26 +314,34 @@ class CommonAdapter(object): lun_name=volume.name, base_lun_name=base_lun_name, pool_name=pool) + location = self._build_provider_location( lun_type='smp', lun_id=new_lun_id, base_lun_name=base_lun_name) volume_metadata['snapcopy'] = 'True' + volume_metadata['async_migrate'] = 'False' else: + async_migrate = utils.is_async_migrate_enabled(volume) + new_snap_name = ( + utils.construct_snap_name(volume) if async_migrate else None) new_lun_id = emc_taskflow.create_volume_from_snapshot( client=self.client, - snap_name=snapshot.name, + src_snap_name=snapshot.name, lun_name=volume.name, lun_size=volume.size, base_lun_name=base_lun_name, pool_name=pool, provision=provision, - tier=tier) + tier=tier, + new_snap_name=new_snap_name) + location = self._build_provider_location( lun_type='lun', lun_id=new_lun_id, base_lun_name=volume.name) volume_metadata['snapcopy'] = 'False' + volume_metadata['async_migrate'] = six.text_type(async_migrate) rep_update = self.setup_lun_replication(volume, new_lun_id) model_update = {'provider_location': location, @@ -349,7 +362,7 @@ class CommonAdapter(object): source_lun_id = self.client.get_lun_id(src_vref) snap_name = utils.construct_snap_name(volume) rep_update = dict() - if utils.snapcopy_enabled(volume): + if utils.is_snapcopy_enabled(volume): # snapcopy feature enabled new_lun_id = emc_taskflow.fast_create_cloned_volume( client=self.client, @@ -362,7 +375,10 @@ class CommonAdapter(object): lun_type='smp', lun_id=new_lun_id, base_lun_name=base_lun_name) + volume_metadata['snapcopy'] = 'True' + volume_metadata['async_migrate'] = 'False' else: + async_migrate = utils.is_async_migrate_enabled(volume) new_lun_id = emc_taskflow.create_cloned_volume( client=self.client, snap_name=snap_name, @@ -372,14 +388,15 @@ class CommonAdapter(object): base_lun_name=base_lun_name, pool_name=pool, provision=provision, - tier=tier) - self.client.delete_snapshot(snap_name) + tier=tier, + async_migrate=async_migrate) # After migration, volume's base lun is itself location = self._build_provider_location( lun_type='lun', lun_id=new_lun_id, base_lun_name=volume.name) volume_metadata['snapcopy'] = 'False' + volume_metadata['async_migrate'] = six.text_type(async_migrate) rep_update = self.setup_lun_replication(volume, new_lun_id) model_update = {'provider_location': location, @@ -720,8 +737,27 @@ class CommonAdapter(object): def delete_volume(self, volume): """Deletes an EMC volume.""" + async_migrate = utils.is_async_migrate_enabled(volume) self.cleanup_lun_replication(volume) - self.client.delete_lun(volume.name, force=self.force_delete_lun_in_sg) + try: + self.client.delete_lun(volume.name, + force=self.force_delete_lun_in_sg) + except storops_ex.VNXLunUsedByFeatureError: + # Case 1. Migration not finished, cleanup related stuff. + if async_migrate: + self.client.cleanup_async_lun( + name=volume.name, + force=self.force_delete_lun_in_sg) + else: + raise + except (storops_ex.VNXLunHasSnapError, + storops_ex.VNXLunHasSnapMountPointError): + # Here, we assume no Cinder managed snaps, and add it to queue + # for later deletion + self.client.delay_delete_lun(volume.name) + # Case 2. Migration already finished, delete temp snap if exists. + if async_migrate: + self.client.delete_snapshot(utils.construct_snap_name(volume)) def extend_volume(self, volume, new_size): """Extends an EMC volume.""" diff --git a/cinder/volume/drivers/dell_emc/vnx/client.py b/cinder/volume/drivers/dell_emc/vnx/client.py index e4c65bf7d7f..3736f57cbbd 100644 --- a/cinder/volume/drivers/dell_emc/vnx/client.py +++ b/cinder/volume/drivers/dell_emc/vnx/client.py @@ -19,9 +19,10 @@ from oslo_utils import importutils storops = importutils.try_import('storops') if storops: from storops import exception as storops_ex + from storops.lib import tasks as storops_tasks from cinder import exception -from cinder.i18n import _, _LW, _LE +from cinder.i18n import _, _LW, _LE, _LI from cinder import utils as cinder_utils from cinder.volume.drivers.dell_emc.vnx import common from cinder.volume.drivers.dell_emc.vnx import const @@ -79,7 +80,7 @@ class Condition(object): class Client(object): def __init__(self, ip, username, password, scope, - naviseccli, sec_file): + naviseccli, sec_file, queue_path=None): self.naviseccli = naviseccli if not storops: msg = _('storops Python library is not installed.') @@ -91,6 +92,10 @@ class Client(object): naviseccli=naviseccli, sec_file=sec_file) self.sg_cache = {} + if queue_path: + self.queue = storops_tasks.PQueue(path=queue_path) + self.queue.start() + LOG.info(_LI('PQueue[%s] starts now.'), queue_path) def create_lun(self, pool, name, size, provision, tier, cg_id=None, ignore_thresholds=False): @@ -138,10 +143,25 @@ class Client(object): if smp_attached_snap: smp_attached_snap.delete() except storops_ex.VNXLunNotFoundError as ex: - LOG.warning(_LW("LUN %(name)s is already deleted. " - "Message: %(msg)s"), - {'name': name, 'msg': ex.message}) - pass # Ignore the failure that due to retry. + LOG.info(_LI("LUN %(name)s is already deleted. This message can " + "be safely ignored. Message: %(msg)s"), + {'name': name, 'msg': ex.message}) + + def cleanup_async_lun(self, name, force=False): + """Helper method to cleanup stuff for async migration. + + .. note:: + Only call it when VNXLunUsedByFeatureError occurs + """ + lun = self.get_lun(name=name) + self.cleanup_migration(src_id=lun.lun_id) + lun.delete(force_detach=True, detach_from_sg=force) + + def delay_delete_lun(self, name): + """Delay the deletion by putting it in a storops queue.""" + self.queue.put(self.vnx.delete_lun, name=name) + LOG.info(_LI("VNX object has been added to queue for later" + " deletion: %s"), name) @cinder_utils.retry(const.VNXLunPreparingError, retries=1, backoff_rate=1) @@ -212,7 +232,7 @@ class Client(object): else: return False - def cleanup_migration(self, src_id, dst_id): + def cleanup_migration(self, src_id, dst_id=None): """Invoke when migration meets error. :param src_id: source LUN id @@ -227,14 +247,20 @@ class Client(object): '%(src_id)s -> %(dst_id)s.'), {'src_id': src_id, 'dst_id': dst_id}) - src_lun.cancel_migrate() + try: + src_lun.cancel_migrate() + except storops_ex.VNXLunNotMigratingError: + LOG.info(_LI('The LUN is not migrating, this message can be' + ' safely ignored')) - def create_snapshot(self, lun_id, snap_name): + def create_snapshot(self, lun_id, snap_name, keep_for=None): """Creates a snapshot.""" lun = self.get_lun(lun_id=lun_id) try: - lun.create_snap(snap_name, allow_rw=True, auto_delete=False) + lun.create_snap( + snap_name, allow_rw=True, auto_delete=False, + keep_for=keep_for) except storops_ex.VNXSnapNameInUseError as ex: LOG.warning(_LW('Snapshot %(name)s already exists. ' 'Message: %(msg)s'), @@ -292,9 +318,11 @@ class Client(object): "currently attached. Message: %(msg)s"), {'smp_name': smp_name, 'msg': ex.message}) - def modify_snapshot(self, snap_name, allow_rw=None, auto_delete=None): + def modify_snapshot(self, snap_name, allow_rw=None, + auto_delete=None, keep_for=None): snap = self.vnx.get_snap(name=snap_name) - snap.modify(allow_rw=allow_rw, auto_delete=auto_delete) + snap.modify(allow_rw=allow_rw, auto_delete=auto_delete, + keep_for=None) def create_consistency_group(self, cg_name, lun_id_list=None): try: diff --git a/cinder/volume/drivers/dell_emc/vnx/common.py b/cinder/volume/drivers/dell_emc/vnx/common.py index d275d3a8ba3..3a1d723fa9e 100644 --- a/cinder/volume/drivers/dell_emc/vnx/common.py +++ b/cinder/volume/drivers/dell_emc/vnx/common.py @@ -38,6 +38,9 @@ INTERVAL_20_SEC = 20 INTERVAL_30_SEC = 30 INTERVAL_60_SEC = 60 +SNAP_EXPIRATION_HOUR = '1h' + + VNX_OPTS = [ cfg.StrOpt('storage_vnx_authentication_type', default='global', @@ -160,7 +163,7 @@ class ExtraSpecs(object): value = enum_class.parse(value) except ValueError: reason = (_("The value %(value)s for key %(key)s in extra " - "specs is invalid."), + "specs is invalid.") % {'key': key, 'value': value}) raise exception.InvalidVolumeType(reason=reason) return value diff --git a/cinder/volume/drivers/dell_emc/vnx/driver.py b/cinder/volume/drivers/dell_emc/vnx/driver.py index 55c3a4af7cc..a5fe4697d65 100644 --- a/cinder/volume/drivers/dell_emc/vnx/driver.py +++ b/cinder/volume/drivers/dell_emc/vnx/driver.py @@ -73,9 +73,10 @@ class VNXDriver(driver.TransferVD, Replication v2 support(managed) Configurable migration rate support 8.0.0 - New VNX Cinder driver + 9.0.0 - Use asynchronous migration for cloning """ - VERSION = '08.00.00' + VERSION = '09.00.00' VENDOR = 'Dell EMC' # ThirdPartySystems wiki page CI_WIKI_NAME = "EMC_VNX_CI" diff --git a/cinder/volume/drivers/dell_emc/vnx/taskflows.py b/cinder/volume/drivers/dell_emc/vnx/taskflows.py index 530b5333a13..340badfbc3f 100644 --- a/cinder/volume/drivers/dell_emc/vnx/taskflows.py +++ b/cinder/volume/drivers/dell_emc/vnx/taskflows.py @@ -24,6 +24,7 @@ from taskflow import task from taskflow.types import failure from cinder import exception +from cinder.volume.drivers.dell_emc.vnx import common from cinder.volume.drivers.dell_emc.vnx import const from cinder.volume.drivers.dell_emc.vnx import utils from cinder.i18n import _, _LI, _LW @@ -37,19 +38,18 @@ class MigrateLunTask(task.Task): Reversion strategy: Cleanup the migration session """ def __init__(self, name=None, provides=None, inject=None, - rebind=None, wait_for_completion=True): + rebind=None): super(MigrateLunTask, self).__init__(name=name, provides=provides, inject=inject, rebind=rebind) - self.wait_for_completion = wait_for_completion - def execute(self, client, src_id, dst_id, *args, **kwargs): + def execute(self, client, src_id, dst_id, async_migrate, *args, **kwargs): LOG.debug('%s.execute', self.__class__.__name__) dst_lun = client.get_lun(lun_id=dst_id) dst_wwn = dst_lun.wwn client.migrate_lun(src_id, dst_id) - if self.wait_for_completion: + if not async_migrate: migrated = client.verify_migration(src_id, dst_id, dst_wwn) if not migrated: msg = _("Failed to migrate volume between source vol %(src)s" @@ -175,12 +175,13 @@ class CreateSnapshotTask(task.Task): Reversion Strategy: Delete the created snapshot. """ - def execute(self, client, snap_name, lun_id, *args, **kwargs): + def execute(self, client, snap_name, lun_id, keep_for=None, + *args, **kwargs): LOG.debug('%s.execute', self.__class__.__name__) LOG.info(_LI('Create snapshot: %(snapshot)s: lun: %(lun)s'), {'snapshot': snap_name, 'lun': lun_id}) - client.create_snapshot(lun_id, snap_name) + client.create_snapshot(lun_id, snap_name, keep_for=keep_for) def revert(self, result, client, snap_name, *args, **kwargs): method_name = '%s.revert' % self.__class__.__name__ @@ -191,11 +192,12 @@ class CreateSnapshotTask(task.Task): client.delete_snapshot(snap_name) -class AllowReadWriteTask(task.Task): +class ModifySnapshotTask(task.Task): """Task to modify a Snapshot to allow ReadWrite on it.""" - def execute(self, client, snap_name, *args, **kwargs): + def execute(self, client, snap_name, keep_for=None, + *args, **kwargs): LOG.debug('%s.execute', self.__class__.__name__) - client.modify_snapshot(snap_name, allow_rw=True) + client.modify_snapshot(snap_name, allow_rw=True, keep_for=keep_for) def revert(self, result, client, snap_name, *args, **kwargs): method_name = '%s.revert' % self.__class__.__name__ @@ -333,6 +335,7 @@ def run_migration_taskflow(client, 'tier': tier, 'ignore_thresholds': True, 'src_id': lun_id, + 'async_migrate': False, } work_flow = linear_flow.Flow(flow_name) work_flow.add(CreateLunTask(), @@ -364,7 +367,7 @@ def fast_create_volume_from_snapshot(client, } work_flow = linear_flow.Flow(flow_name) work_flow.add(CopySnapshotTask(), - AllowReadWriteTask(rebind={'snap_name': 'new_snap_name'}), + ModifySnapshotTask(rebind={'snap_name': 'new_snap_name'}), CreateSMPTask(), AttachSnapTask(rebind={'snap_name': 'new_snap_name'})) engine = taskflow.engines.load( @@ -374,17 +377,19 @@ def fast_create_volume_from_snapshot(client, return lun_id -def create_volume_from_snapshot(client, snap_name, lun_name, +def create_volume_from_snapshot(client, src_snap_name, lun_name, lun_size, base_lun_name, pool_name, - provision, tier): - # Step 1: create smp from base lun - # Step 2: attach snapshot to smp - # Step 3: Create new LUN - # Step 4: migrate the smp to new LUN + provision, tier, new_snap_name=None): + # Step 1: Copy and modify snap(only for async migrate) + # Step 2: Create smp from base lun + # Step 3: Attach snapshot to smp + # Step 4: Create new LUN + # Step 5: migrate the smp to new LUN tmp_lun_name = '%s_dest' % lun_name flow_name = 'create_volume_from_snapshot' store_spec = {'client': client, - 'snap_name': snap_name, + 'snap_name': src_snap_name, + 'new_snap_name': new_snap_name, 'smp_name': lun_name, 'lun_name': tmp_lun_name, 'lun_size': lun_size, @@ -392,10 +397,19 @@ def create_volume_from_snapshot(client, snap_name, lun_name, 'pool_name': pool_name, 'provision': provision, 'tier': tier, + 'keep_for': (common.SNAP_EXPIRATION_HOUR + if new_snap_name else None), + 'async_migrate': True if new_snap_name else False, } work_flow = linear_flow.Flow(flow_name) + if new_snap_name: + work_flow.add(CopySnapshotTask(), + ModifySnapshotTask( + rebind={'snap_name': 'new_snap_name'})) + work_flow.add(CreateSMPTask(), - AttachSnapTask(), + AttachSnapTask(rebind={'snap_name': 'new_snap_name'}) + if new_snap_name else AttachSnapTask(), CreateLunTask(), MigrateLunTask( rebind={'src_id': 'smp_id', @@ -428,7 +442,7 @@ def fast_create_cloned_volume(client, snap_name, lun_id, def create_cloned_volume(client, snap_name, lun_id, lun_name, lun_size, base_lun_name, pool_name, - provision, tier): + provision, tier, async_migrate=False): tmp_lun_name = '%s_dest' % lun_name flow_name = 'create_cloned_volume' store_spec = {'client': client, @@ -441,6 +455,9 @@ def create_cloned_volume(client, snap_name, lun_id, lun_name, 'pool_name': pool_name, 'provision': provision, 'tier': tier, + 'keep_for': (common.SNAP_EXPIRATION_HOUR if + async_migrate else None), + 'async_migrate': async_migrate, } work_flow = linear_flow.Flow(flow_name) work_flow.add( @@ -453,6 +470,8 @@ def create_cloned_volume(client, snap_name, lun_id, lun_name, engine = taskflow.engines.load( work_flow, store=store_spec) engine.run() + if not async_migrate: + client.delete_snapshot(snap_name) lun_id = engine.storage.fetch('smp_id') return lun_id @@ -473,7 +492,7 @@ def create_cg_from_cg_snapshot(client, cg_name, src_cg_name, prepare_tasks.append( CopySnapshotTask()) prepare_tasks.append( - AllowReadWriteTask(rebind={'snap_name': 'new_snap_name'})) + ModifySnapshotTask(rebind={'snap_name': 'new_snap_name'})) else: flow_name = 'create_cg_from_cg' snap_name = cg_snap_name @@ -505,6 +524,7 @@ def create_cg_from_cg_snapshot(client, cg_name, src_cg_name, 'base_lun_name': src_lun_names[i], 'smp_name': lun_name, 'snap_name': snap_name, + 'async_migrate': True, } work_flow.add(CreateSMPTask(name="CreateSMPTask_%s" % i, inject=sub_store_spec, @@ -519,8 +539,7 @@ def create_cg_from_cg_snapshot(client, cg_name, src_cg_name, name="MigrateLunTask_%s" % i, inject=sub_store_spec, rebind={'src_id': new_src_id_template % i, - 'dst_id': new_dst_id_template % i}, - wait_for_completion=False)) + 'dst_id': new_dst_id_template % i})) # Wait all migration session finished work_flow.add(WaitMigrationsTask(new_src_id_template, diff --git a/cinder/volume/drivers/dell_emc/vnx/utils.py b/cinder/volume/drivers/dell_emc/vnx/utils.py index 19af1a3a166..5c0f48300dd 100644 --- a/cinder/volume/drivers/dell_emc/vnx/utils.py +++ b/cinder/volume/drivers/dell_emc/vnx/utils.py @@ -201,7 +201,7 @@ def get_original_status(volume): def construct_snap_name(volume): """Return snapshot name.""" - if snapcopy_enabled(volume): + if is_snapcopy_enabled(volume): return 'snap-as-vol-' + six.text_type(volume.name_id) else: return 'tmp-snap-' + six.text_type(volume.name_id) @@ -227,11 +227,25 @@ def construct_smp_name(snap_id): return 'tmp-smp-' + six.text_type(snap_id) -def snapcopy_enabled(volume): +def is_snapcopy_enabled(volume): meta = get_metadata(volume) return 'snapcopy' in meta and meta['snapcopy'].lower() == 'true' +def is_async_migrate_enabled(volume): + extra_specs = common.ExtraSpecs.from_volume(volume) + if extra_specs.is_replication_enabled: + # For replication-enabled volume, we should not use the async-cloned + # volume, or setup replication would fail with + # VNXMirrorLunNotAvailableError + return False + meta = get_metadata(volume) + if 'async_migrate' not in meta: + # Asynchronous migration is the default behavior now + return True + return 'async_migrate' in meta and meta['async_migrate'].lower() == 'true' + + def get_migration_rate(volume): metadata = get_metadata(volume) rate = metadata.get('migrate_rate', None) diff --git a/releasenotes/notes/vnx-async-migration-support-3c449139bb264004.yaml b/releasenotes/notes/vnx-async-migration-support-3c449139bb264004.yaml new file mode 100644 index 00000000000..5171d2635e6 --- /dev/null +++ b/releasenotes/notes/vnx-async-migration-support-3c449139bb264004.yaml @@ -0,0 +1,10 @@ +--- +features: + - VNX cinder driver now supports async migration during volume cloning. + By default, the cloned volume will be available after the migration starts + in the VNX instead of waiting for the completion of migration. This greatly + accelerates the cloning process. + If user wants to disable this, he could add + ``--metadata async_migrate=False`` when creating volume from source + volume/snapshot. +