From f17984af106695368fc1ff5cdbe9d60882e6d8d5 Mon Sep 17 00:00:00 2001 From: peter_wang Date: Sat, 10 Oct 2015 05:23:17 -0400 Subject: [PATCH] VNX: Replication V2 support(managed) This patch implements the managed side of replication V2 in VNX driver. cinder.conf should follow below examples: [vnx_cinder] ... replication_device = target_device_id:, managed_backend_name: @#, san_ip:192.168.1.2, san_login:admin, san_password:admin, storage_vnx_authentication_type:global, storage_vnx_security_file_dir:/home/stack ... Supported operation: * create volume * disable replication * enable replication * failover replication * clone volume * create volume from snapshot VNX cinder driver also supports failover back and forth NOTE: you can only place one replication_device for each backend since VNX driver only supports 1:1 fanout ratio. Only synchronized replication is supported in this patch. DocImpact Change-Id: Ica47700b0f251bb4f7af5500f36416ddf91de9c5 Implements: blueprint vnx-replication-v2 --- cinder/tests/unit/test_emc_vnx.py | 512 +++++++++++++- cinder/volume/drivers/emc/emc_cli_fc.py | 21 + cinder/volume/drivers/emc/emc_cli_iscsi.py | 21 + cinder/volume/drivers/emc/emc_vnx_cli.py | 638 +++++++++++++++++- .../vnx-replication-v2-2afc4ac0c2ecfa60.yaml | 3 + 5 files changed, 1172 insertions(+), 23 deletions(-) create mode 100644 releasenotes/notes/vnx-replication-v2-2afc4ac0c2ecfa60.yaml diff --git a/cinder/tests/unit/test_emc_vnx.py b/cinder/tests/unit/test_emc_vnx.py index d4f3f2acd..6fc7931d6 100644 --- a/cinder/tests/unit/test_emc_vnx.py +++ b/cinder/tests/unit/test_emc_vnx.py @@ -12,6 +12,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import json import os import re @@ -38,6 +39,9 @@ from mock import patch SUCCEED = ("", 0) FAKE_ERROR_RETURN = ("FAKE ERROR", 255) VERSION = emc_vnx_cli.EMCVnxCliBase.VERSION +build_replication_data = ( + emc_vnx_cli.EMCVnxCliBase._build_replication_driver_data) +REPLICATION_KEYS = emc_vnx_cli.EMCVnxCliBase.REPLICATION_KEYS def build_provider_location(lun_id, lun_type, base_lun_name=None, system=None): @@ -56,6 +60,8 @@ def build_migration_dest_name(src_name): class EMCVNXCLIDriverTestData(object): base_lun_name = 'volume-1' + replication_metadata = {'host': 'host@backendsec#unit_test_pool', + 'system': 'FNM11111'} test_volume = { 'status': 'creating', 'name': 'volume-1', @@ -333,11 +339,58 @@ class EMCVNXCLIDriverTestData(object): 'volume_type': [], '_name_id': None, 'metadata': {}} + test_volume_replication = { + 'migration_status': None, + 'availability_zone': 'nova', + 'id': '5', + 'name_id': None, + 'name': 'volume-5', + 'size': 1, + 'status': 'available', + 'volume_type_id': 'rep_type_id', + 'deleted': False, 'provider_location': + build_provider_location(5, 'lun', 'volume-5'), + 'host': 'ubuntu-server12@array_backend_1#unit_test_pool', + 'source_volid': None, 'provider_auth': None, + 'display_name': 'vol-test05', + 'volume_attachment': [], + 'attach_status': 'detached', + 'volume_type': [], + 'replication_driver_data': '', + 'replication_status': 'enabled', + '_name_id': None, 'metadata': replication_metadata} + + test_replication_failover = { + 'migration_status': None, + 'availability_zone': 'nova', + 'id': '5', + 'name_id': None, + 'name': 'volume-5', + 'size': 1, + 'status': 'available', + 'volume_type_id': 'rep_type_id', + 'deleted': False, 'provider_location': + build_provider_location(5, 'lun', 'volume-5'), + 'host': 'ubuntu-server12@array_backend_1#unit_test_pool', + 'source_volid': None, 'provider_auth': None, + 'display_name': 'vol-test05', + 'volume_attachment': [], + 'attach_status': 'detached', + 'volume_type': [], + 'replication_driver_data': '', + 'replication_status': 'failed-over', + '_name_id': None, 'metadata': replication_metadata} + test_new_type = {'name': 'voltype0', 'qos_specs_id': None, 'deleted': False, 'extra_specs': {'storagetype:provisioning': 'thin'}, 'id': 'f82f28c8-148b-416e-b1ae-32d3c02556c0'} + test_replication_type = {'name': 'rep_type', + 'extra_specs': {'replication_enbled': + ' True'}, + 'id': 'rep_type_id'} + test_diff = {'encryption': {}, 'qos_specs': {}, 'extra_specs': {'storagetype:provisioning': ('thick', 'thin')}} @@ -523,7 +576,8 @@ class EMCVNXCLIDriverTestData(object): "Name of the software package: -FAST " + "Name of the software package: -FASTCache " + "Name of the software package: -ThinProvisioning " - "Name of the software package: -VNXSnapshots", + "Name of the software package: -VNXSnapshots " + "Name of the software package: -MirrorView/S", 0) NDU_LIST_RESULT_WO_LICENSE = ( @@ -791,6 +845,102 @@ class EMCVNXCLIDriverTestData(object): return ('snap', '-group', '-replmember', '-id', cg_name, '-res', ','.join(new_ids)) + # Replication related commands + def MIRROR_CREATE_CMD(self, mirror_name, lun_id): + return ('mirror', '-sync', '-create', '-name', mirror_name, + '-lun', lun_id, '-usewriteintentlog', '-o') + + def MIRROR_DESTROY_CMD(self, mirror_name): + return ('mirror', '-sync', '-destroy', '-name', mirror_name, + '-force', '-o') + + def MIRROR_ADD_IMAGE_CMD(self, mirror_name, sp_ip, lun_id): + return ('mirror', '-sync', '-addimage', '-name', mirror_name, + '-arrayhost', sp_ip, '-lun', lun_id, '-recoverypolicy', + 'auto', '-syncrate', 'high') + + def MIRROR_REMOVE_IMAGE_CMD(self, mirror_name, image_uid): + return ('mirror', '-sync', '-removeimage', '-name', mirror_name, + '-imageuid', image_uid, '-o') + + def MIRROR_FRACTURE_IMAGE_CMD(self, mirror_name, image_uid): + return ('mirror', '-sync', '-fractureimage', '-name', mirror_name, + '-imageuid', image_uid, '-o') + + def MIRROR_SYNC_IMAGE_CMD(self, mirror_name, image_uid): + return ('mirror', '-sync', '-syncimage', '-name', mirror_name, + '-imageuid', image_uid, '-o') + + def MIRROR_PROMOTE_IMAGE_CMD(self, mirror_name, image_uid): + return ('mirror', '-sync', '-promoteimage', '-name', mirror_name, + '-imageuid', image_uid, '-o') + + def MIRROR_LIST_CMD(self, mirror_name): + return ('mirror', '-sync', '-list', '-name', mirror_name) + + # Mirror related output + def MIRROR_LIST_RESULT(self, mirror_name, mirror_state='Synchronized'): + return ("""MirrorView Name: %(name)s +MirrorView Description: +MirrorView UID: 50:06:01:60:B6:E0:1C:F4:0E:00:00:00:00:00:00:00 +Logical Unit Numbers: 37 +Remote Mirror Status: Mirrored +MirrorView State: Active +MirrorView Faulted: NO +MirrorView Transitioning: NO +Quiesce Threshold: 60 +Minimum number of images required: 0 +Image Size: 2097152 +Image Count: 2 +Write Intent Log Used: YES +Images: +Image UID: 50:06:01:60:B6:E0:1C:F4 +Is Image Primary: YES +Logical Unit UID: 60:06:01:60:13:00:3E:00:14:FA:3C:8B:A5:98:E5:11 +Image Condition: Primary Image +Preferred SP: A + +Image UID: 50:06:01:60:88:60:05:FE +Is Image Primary: NO +Logical Unit UID: 60:06:01:60:41:C4:3D:00:B2:D5:33:DB:C7:98:E5:11 +Image State: %(state)s +Image Condition: Normal +Recovery Policy: Automatic +Preferred SP: A +Synchronization Rate: High +Image Faulted: NO +Image Transitioning: NO +Synchronizing Progress(%%): 100 +""" % {'name': mirror_name, 'state': mirror_state}, 0) + + def MIRROR_LIST_ERROR_RESULT(self, mirror_name): + return ("Getting mirror list failed. Mirror not found", 145) + + def MIRROR_CREATE_ERROR_RESULT(self, mirror_name): + return ( + "Error: mirrorview command failed\n" + "Mirror name already in use", 67) + + def MIRROR_DESTROY_ERROR_RESULT(self, mirror_name): + return ("Destroying mirror failed. Mirror not found", 145) + + def MIRROR_ADD_IMAGE_ERROR_RESULT(self): + return ( + "Adding sync mirror image failed. Invalid LUN number\n" + "LUN does not exist or Specified LU not available " + "for mirroring.", 169) + + def MIRROR_PROMOTE_IMAGE_ERROR_RESULT(self): + return ( + "Error: mirrorview command failed\n" + "UID of the secondary image to be promoted is not local to " + "this array.Mirrorview can't promote a secondary image not " + "local to this array. Make sure you are sending the promote " + "command to the correct array where the secondary image is " + "located. (0x7105824e)", 78) + + # Test Objects + def CONSISTENCY_GROUP_VOLUMES(self): volumes = [] volumes.append(self.test_volume) @@ -1371,7 +1521,7 @@ class DriverTestCaseBase(test.TestCase): self.fake_command_execute_for_driver_setup) self.stubs.Set(emc_vnx_cli.CommandLineHelper, 'get_array_serial', mock.Mock(return_value={'array_serial': - 'fakeSerial'})) + 'fake_serial'})) self.stubs.Set(os.path, 'exists', mock.Mock(return_value=1)) self.stubs.Set(emc_vnx_cli, 'INTERVAL_5_SEC', 0.01) @@ -1763,7 +1913,7 @@ class EMCVNXCLIDriverISCSITestCase(DriverTestCaseBase): expected_pool_stats = { 'free_capacity_gb': 3105.303, 'reserved_percentage': 32, - 'location_info': 'unit_test_pool|fakeSerial', + 'location_info': 'unit_test_pool|fake_serial', 'total_capacity_gb': 3281.146, 'provisioned_capacity_gb': 536.14, 'compression_support': 'True', @@ -1772,6 +1922,7 @@ class EMCVNXCLIDriverISCSITestCase(DriverTestCaseBase): 'thick_provisioning_support': True, 'max_over_subscription_ratio': 20.0, 'consistencygroup_support': 'True', + 'replication_enabled': False, 'pool_name': 'unit_test_pool', 'fast_cache_enabled': True, 'fast_support': 'True'} @@ -1865,7 +2016,7 @@ Time Remaining: 0 second(s) 23)]] fake_cli = self.driverSetup(commands, results) fakehost = {'capabilities': {'location_info': - "unit_test_pool2|fakeSerial", + "unit_test_pool2|fake_serial", 'storage_protocol': 'iSCSI'}} ret = self.driver.migrate_volume(None, self.testData.test_volume, fakehost)[0] @@ -1910,7 +2061,7 @@ Time Remaining: 0 second(s) 'currently migrating', 23)]] fake_cli = self.driverSetup(commands, results) fake_host = {'capabilities': {'location_info': - "unit_test_pool2|fakeSerial", + "unit_test_pool2|fake_serial", 'storage_protocol': 'iSCSI'}} ret = self.driver.migrate_volume(None, self.testData.test_volume, fake_host)[0] @@ -1953,7 +2104,7 @@ Time Remaining: 0 second(s) 23)]] fake_cli = self.driverSetup(commands, results) fakehost = {'capabilities': {'location_info': - "unit_test_pool2|fakeSerial", + "unit_test_pool2|fake_serial", 'storage_protocol': 'iSCSI'}} ret = self.driver.migrate_volume(None, self.testData.test_volume5, fakehost)[0] @@ -1981,7 +2132,7 @@ Time Remaining: 0 second(s) results = [FAKE_ERROR_RETURN] fake_cli = self.driverSetup(commands, results) fakehost = {'capabilities': {'location_info': - "unit_test_pool2|fakeSerial", + "unit_test_pool2|fake_serial", 'storage_protocol': 'iSCSI'}} ret = self.driver.migrate_volume(None, self.testData.test_volume, fakehost)[0] @@ -2013,7 +2164,7 @@ Time Remaining: 0 second(s) SUCCEED] fake_cli = self.driverSetup(commands, results) fake_host = {'capabilities': {'location_info': - "unit_test_pool2|fakeSerial", + "unit_test_pool2|fake_serial", 'storage_protocol': 'iSCSI'}} self.assertRaisesRegex(exception.VolumeBackendAPIException, @@ -2066,7 +2217,7 @@ Time Remaining: 0 second(s) 'currently migrating', 23)]] fake_cli = self.driverSetup(commands, results) fake_host = {'capabilities': {'location_info': - "unit_test_pool2|fakeSerial", + "unit_test_pool2|fake_serial", 'storage_protocol': 'iSCSI'}} vol = EMCVNXCLIDriverTestData.convert_volume( @@ -2941,6 +3092,10 @@ Time Remaining: 0 second(s) mock.call(*self.testData.LUN_DELETE_CMD('volume-5'))] fake_cli.assert_has_calls(expected) + @mock.patch( + "cinder.volume.volume_types." + "get_volume_type_extra_specs", + mock.Mock(return_value={'storagetype:provisioning': 'compressed'})) def test_delete_volume_smp(self): fake_cli = self.driverSetup() vol = self.testData.test_volume_with_type.copy() @@ -4823,7 +4978,7 @@ class EMCVNXCLIDArrayBasedDriverTestCase(DriverTestCaseBase): expected_pool_stats1 = { 'free_capacity_gb': 3105.303, 'reserved_percentage': 32, - 'location_info': 'unit_test_pool|fakeSerial', + 'location_info': 'unit_test_pool|fake_serial', 'total_capacity_gb': 3281.146, 'provisioned_capacity_gb': 536.140, 'compression_support': 'True', @@ -4831,6 +4986,7 @@ class EMCVNXCLIDArrayBasedDriverTestCase(DriverTestCaseBase): 'thin_provisioning_support': True, 'thick_provisioning_support': True, 'consistencygroup_support': 'True', + 'replication_enabled': False, 'pool_name': 'unit_test_pool', 'max_over_subscription_ratio': 20.0, 'fast_cache_enabled': True, @@ -4841,7 +4997,7 @@ class EMCVNXCLIDArrayBasedDriverTestCase(DriverTestCaseBase): expected_pool_stats2 = { 'free_capacity_gb': 3984.768, 'reserved_percentage': 32, - 'location_info': 'unit_test_pool2|fakeSerial', + 'location_info': 'unit_test_pool2|fake_serial', 'total_capacity_gb': 4099.992, 'provisioned_capacity_gb': 636.240, 'compression_support': 'True', @@ -4849,6 +5005,7 @@ class EMCVNXCLIDArrayBasedDriverTestCase(DriverTestCaseBase): 'thin_provisioning_support': True, 'thick_provisioning_support': True, 'consistencygroup_support': 'True', + 'replication_enabled': False, 'pool_name': 'unit_test_pool2', 'max_over_subscription_ratio': 20.0, 'fast_cache_enabled': False, @@ -4869,7 +5026,7 @@ class EMCVNXCLIDArrayBasedDriverTestCase(DriverTestCaseBase): expected_pool_stats1 = { 'free_capacity_gb': 3105.303, 'reserved_percentage': 32, - 'location_info': 'unit_test_pool|fakeSerial', + 'location_info': 'unit_test_pool|fake_serial', 'total_capacity_gb': 3281.146, 'provisioned_capacity_gb': 536.140, 'compression_support': 'False', @@ -4878,6 +5035,7 @@ class EMCVNXCLIDArrayBasedDriverTestCase(DriverTestCaseBase): 'thick_provisioning_support': True, 'consistencygroup_support': 'False', 'pool_name': 'unit_test_pool', + 'replication_enabled': False, 'max_over_subscription_ratio': 20.0, 'fast_cache_enabled': 'False', 'fast_support': 'False'} @@ -4887,7 +5045,7 @@ class EMCVNXCLIDArrayBasedDriverTestCase(DriverTestCaseBase): expected_pool_stats2 = { 'free_capacity_gb': 3984.768, 'reserved_percentage': 32, - 'location_info': 'unit_test_pool2|fakeSerial', + 'location_info': 'unit_test_pool2|fake_serial', 'total_capacity_gb': 4099.992, 'provisioned_capacity_gb': 636.240, 'compression_support': 'False', @@ -4895,6 +5053,7 @@ class EMCVNXCLIDArrayBasedDriverTestCase(DriverTestCaseBase): 'thin_provisioning_support': False, 'thick_provisioning_support': True, 'consistencygroup_support': 'False', + 'replication_enabled': False, 'pool_name': 'unit_test_pool2', 'max_over_subscription_ratio': 20.0, 'fast_cache_enabled': 'False', @@ -5415,7 +5574,7 @@ class EMCVNXCLIDriverFCTestCase(DriverTestCaseBase): expected_pool_stats = { 'free_capacity_gb': 3105.303, 'reserved_percentage': 32, - 'location_info': 'unit_test_pool|fakeSerial', + 'location_info': 'unit_test_pool|fake_serial', 'total_capacity_gb': 3281.146, 'provisioned_capacity_gb': 536.14, 'compression_support': 'True', @@ -5424,6 +5583,7 @@ class EMCVNXCLIDriverFCTestCase(DriverTestCaseBase): 'thick_provisioning_support': True, 'max_over_subscription_ratio': 20.0, 'consistencygroup_support': 'True', + 'replication_enabled': False, 'pool_name': 'unit_test_pool', 'fast_cache_enabled': True, 'fast_support': 'True'} @@ -5767,6 +5927,330 @@ class EMCVNXCLIMultiPoolsTestCase(DriverTestCaseBase): self.assertEqual(set(), driver.cli.storage_pools) + +@patch.object(emc_vnx_cli.EMCVnxCliBase, + 'enablers', + mock.PropertyMock(return_value=['-MirrorView/S'])) +class EMCVNXCLIDriverReplicationV2TestCase(DriverTestCaseBase): + def setUp(self): + super(EMCVNXCLIDriverReplicationV2TestCase, self).setUp() + self.target_device_id = 'fake_serial' + self.configuration.replication_device = [{ + 'target_device_id': self.target_device_id, + 'managed_backend_name': 'host@backend#unit_test_pool', + 'san_ip': '192.168.1.2', 'san_login': 'admin', + 'san_password': 'admin', 'san_secondary_ip': '192.168.2.2', + 'storage_vnx_authentication_type': 'global', + 'storage_vnx_security_file_dir': None}] + + def generate_driver(self, conf): + return emc_cli_iscsi.EMCCLIISCSIDriver(configuration=conf) + + def _build_mirror_name(self, volume_id): + return 'mirror_' + volume_id + + @mock.patch( + "cinder.volume.volume_types." + "get_volume_type_extra_specs", + mock.Mock(return_value={'replication_enabled': ' True'})) + def test_create_volume_with_replication(self): + rep_volume = EMCVNXCLIDriverTestData.convert_volume( + self.testData.test_volume_replication) + mirror_name = self._build_mirror_name(rep_volume.id) + commands = [self.testData.MIRROR_CREATE_CMD(mirror_name, 5), + self.testData.MIRROR_ADD_IMAGE_CMD( + mirror_name, '192.168.1.2', 5)] + results = [SUCCEED, SUCCEED] + fake_cli = self.driverSetup(commands, results) + self.driver.cli.enablers.append('-MirrorView/S') + with mock.patch.object( + emc_vnx_cli.CommandLineHelper, + 'create_lun_with_advance_feature', + mock.Mock(return_value={'lun_id': 5})): + model_update = self.driver.create_volume(rep_volume) + self.assertTrue(model_update['replication_status'] == 'enabled') + self.assertTrue(model_update['replication_driver_data'] == + build_replication_data(self.configuration)) + self.assertDictMatch({'system': self.target_device_id, + 'host': rep_volume.host, + 'snapcopy': 'False'}, + model_update['metadata']) + fake_cli.assert_has_calls( + [mock.call(*self.testData.MIRROR_CREATE_CMD(mirror_name, 5), + poll=True), + mock.call(*self.testData.MIRROR_ADD_IMAGE_CMD( + mirror_name, '192.168.1.2', 5), poll=True)]) + + @mock.patch( + "cinder.volume.volume_types." + "get_volume_type_extra_specs", + mock.Mock(return_value={'replication_enabled': ' True'})) + def test_create_replication_mirror_exists(self): + rep_volume = EMCVNXCLIDriverTestData.convert_volume( + self.testData.test_volume_replication) + mirror_name = self._build_mirror_name(rep_volume.id) + commands = [self.testData.MIRROR_CREATE_CMD(mirror_name, 5), + self.testData.MIRROR_ADD_IMAGE_CMD( + mirror_name, '192.168.1.2', 5)] + results = [self.testData.MIRROR_CREATE_ERROR_RESULT(mirror_name), + SUCCEED] + fake_cli = self.driverSetup(commands, results) + self.driver.cli.enablers.append('-MirrorView/S') + with mock.patch.object( + emc_vnx_cli.CommandLineHelper, + 'create_lun_with_advance_feature', + mock.Mock(return_value={'lun_id': 5})): + model_update = self.driver.create_volume(rep_volume) + self.assertTrue(model_update['replication_status'] == 'enabled') + self.assertTrue(model_update['replication_driver_data'] == + build_replication_data(self.configuration)) + fake_cli.assert_has_calls( + [mock.call(*self.testData.MIRROR_CREATE_CMD(mirror_name, 5), + poll=True), + mock.call(*self.testData.MIRROR_ADD_IMAGE_CMD( + mirror_name, '192.168.1.2', 5), poll=True)]) + + @mock.patch( + "cinder.volume.volume_types." + "get_volume_type_extra_specs", + mock.Mock(return_value={'replication_enabled': ' True'})) + def test_create_replication_add_image_error(self): + rep_volume = EMCVNXCLIDriverTestData.convert_volume( + self.testData.test_volume_replication) + mirror_name = self._build_mirror_name(rep_volume.id) + commands = [self.testData.MIRROR_CREATE_CMD(mirror_name, 5), + self.testData.MIRROR_ADD_IMAGE_CMD( + mirror_name, '192.168.1.2', 5), + self.testData.LUN_DELETE_CMD(rep_volume.name), + self.testData.MIRROR_DESTROY_CMD(mirror_name)] + results = [SUCCEED, + ("Add Image Error", 25), + SUCCEED, SUCCEED] + fake_cli = self.driverSetup(commands, results) + self.driver.cli._mirror._secondary_client.command_execute = fake_cli + with mock.patch.object( + emc_vnx_cli.CommandLineHelper, + 'create_lun_with_advance_feature', + mock.Mock(return_value={'lun_id': 5})): + self.assertRaisesRegex(exception.EMCVnxCLICmdError, + 'Add Image Error', + self.driver.create_volume, + rep_volume) + + fake_cli.assert_has_calls( + [mock.call(*self.testData.MIRROR_CREATE_CMD(mirror_name, 5), + poll=True), + mock.call(*self.testData.MIRROR_ADD_IMAGE_CMD( + mirror_name, '192.168.1.2', 5), poll=True), + mock.call(*self.testData.LUN_DELETE_CMD(rep_volume.name)), + mock.call(*self.testData.MIRROR_DESTROY_CMD(mirror_name), + poll=True)]) + + def test_enable_replication(self): + rep_volume = EMCVNXCLIDriverTestData.convert_volume( + self.testData.test_volume_replication) + mirror_name = self._build_mirror_name(rep_volume.id) + image_uid = '50:06:01:60:88:60:05:FE' + commands = [self.testData.MIRROR_LIST_CMD(mirror_name), + self.testData.MIRROR_SYNC_IMAGE_CMD( + mirror_name, image_uid)] + results = [[self.testData.MIRROR_LIST_RESULT( + mirror_name, 'Administratively fractured'), + self.testData.MIRROR_LIST_RESULT( + mirror_name)], + SUCCEED] + fake_cli = self.driverSetup(commands, results) + rep_volume.replication_driver_data = build_replication_data( + self.configuration) + self.driver.cli._mirror._secondary_client.command_execute = fake_cli + self.driver.replication_enable(None, rep_volume) + fake_cli.assert_has_calls([ + mock.call(*self.testData.MIRROR_LIST_CMD(mirror_name), + poll=True), + mock.call(*self.testData.MIRROR_SYNC_IMAGE_CMD( + mirror_name, image_uid), poll=False), + mock.call(*self.testData.MIRROR_LIST_CMD(mirror_name), + poll=False)]) + + def test_enable_already_synced(self): + rep_volume = EMCVNXCLIDriverTestData.convert_volume( + self.testData.test_volume_replication) + mirror_name = self._build_mirror_name(rep_volume.id) + commands = [self.testData.MIRROR_LIST_CMD(mirror_name)] + results = [self.testData.MIRROR_LIST_RESULT(mirror_name)] + fake_cli = self.driverSetup(commands, results) + rep_volume.replication_driver_data = build_replication_data( + self.configuration) + self.driver.cli._mirror._secondary_client.command_execute = fake_cli + self.driver.replication_enable(None, rep_volume) + fake_cli.assert_has_calls([ + mock.call(*self.testData.MIRROR_LIST_CMD(mirror_name), + poll=True)]) + + def test_disable_replication(self): + rep_volume = EMCVNXCLIDriverTestData.convert_volume( + self.testData.test_volume_replication) + mirror_name = self._build_mirror_name(rep_volume.id) + image_uid = '50:06:01:60:88:60:05:FE' + commands = [self.testData.MIRROR_LIST_CMD(mirror_name), + self.testData.MIRROR_FRACTURE_IMAGE_CMD( + mirror_name, image_uid)] + results = [self.testData.MIRROR_LIST_RESULT(mirror_name), + SUCCEED] + fake_cli = self.driverSetup(commands, results) + rep_volume.replication_driver_data = build_replication_data( + self.configuration) + self.driver.cli._mirror._secondary_client.command_execute = fake_cli + self.driver.replication_disable(None, rep_volume) + fake_cli.assert_has_calls([ + mock.call(*self.testData.MIRROR_LIST_CMD(mirror_name), + poll=True), + mock.call(*self.testData.MIRROR_FRACTURE_IMAGE_CMD(mirror_name, + image_uid), poll=False)]) + + @mock.patch( + "cinder.volume.drivers.emc.emc_vnx_cli.CommandLineHelper." + + "get_lun_by_name", + mock.Mock(return_value={'lun_id': 1})) + def test_failover_replication_from_primary(self): + rep_volume = EMCVNXCLIDriverTestData.convert_volume( + self.testData.test_volume_replication) + mirror_name = self._build_mirror_name(rep_volume.id) + image_uid = '50:06:01:60:88:60:05:FE' + commands = [self.testData.MIRROR_LIST_CMD(mirror_name), + self.testData.MIRROR_PROMOTE_IMAGE_CMD( + mirror_name, image_uid)] + results = [self.testData.MIRROR_LIST_RESULT(mirror_name), + SUCCEED] + fake_cli = self.driverSetup(commands, results) + rep_volume.replication_driver_data = build_replication_data( + self.configuration) + rep_volume.metadata = self.testData.replication_metadata + self.driver.cli._mirror._secondary_client.command_execute = fake_cli + model_update = self.driver.replication_failover( + None, rep_volume, + self.target_device_id) + fake_cli.assert_has_calls([ + mock.call(*self.testData.MIRROR_LIST_CMD(mirror_name), + poll=True), + mock.call(*self.testData.MIRROR_PROMOTE_IMAGE_CMD(mirror_name, + image_uid), poll=False)]) + self.assertEqual( + self.configuration.replication_device[0]['managed_backend_name'], + model_update['host']) + self.assertEqual( + build_provider_location( + '1', 'lun', rep_volume.name, + self.target_device_id), + model_update['provider_location']) + + def test_failover_replication_from_secondary(self): + rep_volume = EMCVNXCLIDriverTestData.convert_volume( + self.testData.test_volume_replication) + mirror_name = self._build_mirror_name(rep_volume.id) + image_uid = '50:06:01:60:88:60:05:FE' + commands = [self.testData.MIRROR_LIST_CMD(mirror_name), + self.testData.MIRROR_PROMOTE_IMAGE_CMD( + mirror_name, image_uid)] + results = [self.testData.MIRROR_LIST_RESULT(mirror_name), + SUCCEED] + fake_cli = self.driverSetup(commands, results) + rep_volume.replication_driver_data = build_replication_data( + self.configuration) + rep_volume.metadata = self.testData.replication_metadata + driver_data = json.loads(rep_volume.replication_driver_data) + driver_data['is_primary'] = False + rep_volume.replication_driver_data = json.dumps(driver_data) + self.driver.cli._mirror._secondary_client.command_execute = fake_cli + with mock.patch( + 'cinder.volume.drivers.emc.emc_vnx_cli.CommandLineHelper') \ + as fake_remote: + fake_remote.return_value = self.driver.cli._client + self.driver.replication_failover(None, rep_volume, + 'FNM11111') + fake_cli.assert_has_calls([ + mock.call(*self.testData.MIRROR_LIST_CMD(mirror_name), + poll=True), + mock.call(*self.testData.MIRROR_PROMOTE_IMAGE_CMD(mirror_name, + image_uid), poll=False)]) + + def test_failover_already_promoted(self): + rep_volume = EMCVNXCLIDriverTestData.convert_volume( + self.testData.test_volume_replication) + mirror_name = self._build_mirror_name(rep_volume.id) + image_uid = '50:06:01:60:88:60:05:FE' + commands = [self.testData.MIRROR_LIST_CMD(mirror_name), + self.testData.MIRROR_PROMOTE_IMAGE_CMD( + mirror_name, image_uid)] + results = [self.testData.MIRROR_LIST_RESULT(mirror_name), + self.testData.MIRROR_PROMOTE_IMAGE_ERROR_RESULT()] + fake_cli = self.driverSetup(commands, results) + rep_volume.replication_driver_data = build_replication_data( + self.configuration) + rep_volume.metadata = self.testData.replication_metadata + self.driver.cli._mirror._secondary_client.command_execute = fake_cli + self.assertRaisesRegex(exception.EMCVnxCLICmdError, + 'UID of the secondary image ' + 'to be promoted is not local', + self.driver.replication_failover, + None, rep_volume, self.target_device_id) + fake_cli.assert_has_calls([ + mock.call(*self.testData.MIRROR_LIST_CMD(mirror_name), + poll=True), + mock.call(*self.testData.MIRROR_PROMOTE_IMAGE_CMD(mirror_name, + image_uid), poll=False)]) + + @mock.patch( + "cinder.volume.volume_types." + "get_volume_type_extra_specs", + mock.Mock(return_value={'replication_enabled': ' True'})) + def test_delete_volume_with_rep(self): + rep_volume = EMCVNXCLIDriverTestData.convert_volume( + self.testData.test_volume_replication) + mirror_name = self._build_mirror_name(rep_volume.id) + image_uid = '50:06:01:60:88:60:05:FE' + commands = [self.testData.MIRROR_LIST_CMD(mirror_name), + self.testData.MIRROR_FRACTURE_IMAGE_CMD(mirror_name, + image_uid), + self.testData.MIRROR_REMOVE_IMAGE_CMD(mirror_name, + image_uid), + self.testData.MIRROR_DESTROY_CMD(mirror_name)] + results = [self.testData.MIRROR_LIST_RESULT(mirror_name), + SUCCEED, SUCCEED, SUCCEED] + fake_cli = self.driverSetup(commands, results) + self.driver.cli._mirror._secondary_client.command_execute = fake_cli + vol = EMCVNXCLIDriverTestData.convert_volume( + self.testData.test_volume_replication) + vol.replication_driver_data = build_replication_data( + self.configuration) + with mock.patch.object( + emc_vnx_cli.CommandLineHelper, + 'delete_lun', + mock.Mock(return_value=None)): + self.driver.delete_volume(vol) + expected = [mock.call(*self.testData.MIRROR_LIST_CMD(mirror_name), + poll=False), + mock.call(*self.testData.MIRROR_FRACTURE_IMAGE_CMD( + mirror_name, image_uid), poll=False), + mock.call(*self.testData.MIRROR_REMOVE_IMAGE_CMD( + mirror_name, image_uid), poll=False), + mock.call(*self.testData.MIRROR_DESTROY_CMD(mirror_name), + poll=False)] + fake_cli.assert_has_calls(expected) + + def test_list_replication_targets(self): + rep_volume = EMCVNXCLIDriverTestData.convert_volume( + self.testData.test_volume_replication) + rep_volume.replication_driver_data = build_replication_data( + self.configuration) + expect_targets = {'volume_id': rep_volume.id, + 'targets': + [{'type': 'managed', + 'target_device_id': self.target_device_id}]} + self.driverSetup([], []) + data = self.driver.list_replication_targets(None, rep_volume) + self.assertDictMatch(expect_targets, data) + VNXError = emc_vnx_cli.VNXError diff --git a/cinder/volume/drivers/emc/emc_cli_fc.py b/cinder/volume/drivers/emc/emc_cli_fc.py index 37737b51c..749a40bef 100644 --- a/cinder/volume/drivers/emc/emc_cli_fc.py +++ b/cinder/volume/drivers/emc/emc_cli_fc.py @@ -59,6 +59,7 @@ class EMCCLIFCDriver(driver.FibreChannelDriver): Snap copy support Support efficient non-disruptive backup 7.0.0 - Clone consistency group support + Replication v2 support(managed) """ def __init__(self, *args, **kwargs): @@ -301,3 +302,23 @@ class EMCCLIFCDriver(driver.FibreChannelDriver): def backup_use_temp_snapshot(self): return True + + def replication_enable(self, context, volume): + """Enables replication on a replication capable volume.""" + return self.cli.replication_enable(context, volume) + + def replication_disable(self, context, volume): + """Disables replication on a replication-enabled volume.""" + return self.cli.replication_disable(context, volume) + + def replication_failover(self, context, volume, secondary): + """Failovers volume from primary device to secondary.""" + return self.cli.replication_failover(context, volume, secondary) + + def get_replication_updates(self, context): + """Returns updated replication status from volumes.""" + return self.cli.get_replication_updates(context) + + def list_replication_targets(self, context, volume): + """Returns volume replication info.""" + return self.cli.list_replication_targets(context, volume) diff --git a/cinder/volume/drivers/emc/emc_cli_iscsi.py b/cinder/volume/drivers/emc/emc_cli_iscsi.py index eb6bc221b..1acf3defe 100644 --- a/cinder/volume/drivers/emc/emc_cli_iscsi.py +++ b/cinder/volume/drivers/emc/emc_cli_iscsi.py @@ -57,6 +57,7 @@ class EMCCLIISCSIDriver(driver.ISCSIDriver): Snap copy support Support efficient non-disruptive backup 7.0.0 - Clone consistency group support + Replication v2 support(managed) """ def __init__(self, *args, **kwargs): @@ -280,3 +281,23 @@ class EMCCLIISCSIDriver(driver.ISCSIDriver): def backup_use_temp_snapshot(self): return True + + def replication_enable(self, context, volume): + """Enables replication on a replication capable volume.""" + return self.cli.replication_enable(context, volume) + + def replication_disable(self, context, volume): + """Disables replication on a replication-enabled volume.""" + return self.cli.replication_disable(context, volume) + + def replication_failover(self, context, volume, secondary): + """Failovers volume from primary device to secondary.""" + return self.cli.replication_failover(context, volume, secondary) + + def get_replication_updates(self, context): + """Returns updated replication status from volumes.""" + return self.cli.get_replication_updates(context) + + def list_replication_targets(self, context, volume): + """Returns volume replication info.""" + return self.cli.list_replication_targets(context, volume) diff --git a/cinder/volume/drivers/emc/emc_vnx_cli.py b/cinder/volume/drivers/emc/emc_vnx_cli.py index 2a247bf50..4d7c0747b 100644 --- a/cinder/volume/drivers/emc/emc_vnx_cli.py +++ b/cinder/volume/drivers/emc/emc_vnx_cli.py @@ -15,6 +15,7 @@ """ VNX CLI """ +import copy import math import os import random @@ -243,6 +244,8 @@ class VNXError(_Enum): SNAP_ALREADY_MOUNTED = 0x716d8055 SNAP_NOT_ATTACHED = ('The specified Snapshot mount point ' 'is not currently attached.') + MIRROR_NOT_FOUND = 'Mirror not found' + MIRROR_IN_USE = 'Mirror name already in use' @staticmethod def _match(output, error_code): @@ -2111,6 +2114,10 @@ class EMCVnxCliBase(object): 'deduplication_support': 'False', 'thin_provisioning_support': False, 'thick_provisioning_support': True} + REPLICATION_KEYS = ['san_ip', 'san_login', 'san_password', + 'san_secondary_ip', + 'storage_vnx_authentication_type', + 'storage_vnx_security_file_dir'] enablers = [] tmp_snap_prefix = 'tmp-snap-' tmp_smp_for_backup_prefix = 'tmp-smp-' @@ -2144,6 +2151,17 @@ class EMCVnxCliBase(object): "Please register initiator manually.")) self.hlu_set = set(range(1, self.max_luns_per_sg + 1)) self._client = CommandLineHelper(self.configuration) + # Create connection to the secondary storage device + self._mirror = self._build_mirror_view() + self.update_enabler_in_volume_stats() + # Fail the driver if configuration is not correct + if self._mirror: + if '-MirrorView/S' not in self.enablers: + no_enabler_err = _('MirrorView/S enabler is not installed.') + raise exception.VolumeBackendAPIException(data=no_enabler_err) + else: + self._mirror = None + conf_pools = self.configuration.safe_get("storage_vnx_pool_names") self.storage_pools = self._get_managed_storage_pools(conf_pools) self.array_serial = None @@ -2296,6 +2314,9 @@ class EMCVnxCliBase(object): def _construct_tmp_smp_name(self, snapshot): return self.tmp_smp_for_backup_prefix + snapshot.id + def _construct_mirror_name(self, volume): + return 'mirror_' + volume.id + def create_volume(self, volume): """Creates a EMC volume.""" volume_size = volume['size'] @@ -2330,9 +2351,14 @@ class EMCVnxCliBase(object): poll=False) pl = self._build_provider_location(lun_id=data['lun_id'], base_lun_name=volume['name']) + # Setup LUN Replication/MirrorView between devices, + # secondary LUN will inherit properties from primary LUN. + rep_update, metadata_update = self.setup_lun_replication( + volume, data['lun_id'], provisioning, tiering) + volume_metadata.update(metadata_update) model_update = {'provider_location': pl, 'metadata': volume_metadata} - + model_update.update(rep_update) return model_update def _volume_creation_check(self, volume): @@ -2434,6 +2460,8 @@ class EMCVnxCliBase(object): def delete_volume(self, volume, force_delete=False): """Deletes an EMC volume.""" + if self._is_replication_enabled(volume): + self.cleanup_lun_replication(volume) try: self._client.delete_lun(volume['name']) except exception.EMCVnxCLICmdError as ex: @@ -2787,7 +2815,13 @@ class EMCVnxCliBase(object): self.stats['consistencygroup_support']) pool_stats['max_over_subscription_ratio'] = ( self.max_over_subscription_ratio) - + # Add replication V2 support + if self._mirror: + pool_stats['replication_enabled'] = True + pool_stats['replication_count'] = 1 + pool_stats['replication_type'] = ['sync'] + else: + pool_stats['replication_enabled'] = False return pool_stats def update_enabler_in_volume_stats(self): @@ -2809,6 +2843,7 @@ class EMCVnxCliBase(object): self.stats['consistencygroup_support'] = ( 'True' if '-VNXSnapshots' in self.enablers else 'False') + return self.stats def create_snapshot(self, snapshot): @@ -2870,7 +2905,12 @@ class EMCVnxCliBase(object): store_spec = self._construct_store_spec(volume, snapshot) store_spec.update({'base_lun_name': base_lun_name}) volume_metadata = self._get_volume_metadata(volume) + rep_update = {} if self._is_snapcopy_enabled(volume): + if self._is_replication_enabled(volume): + err_msg = _("Unable to enable replication " + "and snapcopy at the same time.") + raise exception.VolumeBackendAPIException(data=err_msg) work_flow.add(CopySnapshotTask(), AllowReadWriteOnSnapshotTask(), CreateSMPTask(), @@ -2894,8 +2934,16 @@ class EMCVnxCliBase(object): pl = self._build_provider_location( new_lun_id, 'lun', volume['name']) volume_metadata['snapcopy'] = 'False' + # Setup LUN Replication/MirrorView between devices, + # secondary LUN will inherit properties from primary LUN. + rep_update, metadata_update = self.setup_lun_replication( + volume, new_lun_id, + store_spec['provisioning'], + store_spec['tiering']) + volume_metadata.update(metadata_update) model_update = {'provider_location': pl, 'metadata': volume_metadata} + model_update.update(rep_update) volume_host = volume['host'] host = vol_utils.extract_host(volume_host, 'backend') host_and_pool = vol_utils.append_host(host, store_spec['pool_name']) @@ -2933,8 +2981,13 @@ class EMCVnxCliBase(object): store_spec.update({'source_lun_id': source_lun_id}) store_spec.update({'base_lun_name': base_lun_name}) volume_metadata = self._get_volume_metadata(volume) + rep_update = {} if self._is_snapcopy_enabled(volume): # snapcopy feature enabled + if self._is_replication_enabled(volume): + err_msg = _("Unable to enable replication " + "and snapcopy at the same time.") + raise exception.VolumeBackendAPIException(data=err_msg) work_flow.add(CreateSnapshotTask(), CreateSMPTask(), AttachSnapTask()) @@ -2965,8 +3018,17 @@ class EMCVnxCliBase(object): new_lun_id, 'lun', volume['name']) volume_metadata['snapcopy'] = 'False' + # Setup LUN Replication/MirrorView between devices, + # secondary LUN will inherit properties from primary LUN. + rep_update, metadata_update = self.setup_lun_replication( + volume, new_lun_id, + store_spec['provisioning'], + store_spec['tiering']) + volume_metadata.update(metadata_update) + model_update = {'provider_location': pl, 'metadata': volume_metadata} + model_update.update(rep_update) volume_host = volume['host'] host = vol_utils.extract_host(volume_host, 'backend') host_and_pool = vol_utils.append_host(host, store_spec['pool_name']) @@ -3006,7 +3068,7 @@ class EMCVnxCliBase(object): :param lun_id: LUN ID in VNX :param type: 'lun' or 'smp' - "param base_lun_name: primary LUN name, + :param base_lun_name: primary LUN name, it will be used when creating snap lun """ pl_dict = {'system': self.get_array_serial(), @@ -3016,6 +3078,13 @@ class EMCVnxCliBase(object): 'version': self.VERSION} return self.dumps_provider_location(pl_dict) + def _update_provider_location(self, provider_location, + key=None, value=None): + pl_dict = {tp.split('^')[0]: tp.split('^')[1] + for tp in provider_location.split('|')} + pl_dict[key] = value + return self.dumps_provider_location(pl_dict) + def _extract_provider_location(self, provider_location, key='id'): """Extracts value of the specified field from provider_location string. @@ -3877,6 +3946,204 @@ class EMCVnxCliBase(object): return specs + def replication_enable(self, context, volume): + """Enables replication for the volume.""" + mirror_name = self._construct_mirror_name(volume) + mirror_view = self._get_mirror_view(volume) + mirror_view.sync_image(mirror_name) + + def replication_disable(self, context, volume): + """Disables replication for the volume.""" + mirror_name = self._construct_mirror_name(volume) + mirror_view = self._get_mirror_view(volume) + mirror_view.fracture_image(mirror_name) + + def replication_failover(self, context, volume, secondary): + """Fails over the volume back and forth. + + Driver needs to update following info for this volume: + 1. host: to point to the new host + 2. provider_location: update serial number and lun id + """ + rep_data = json.loads(volume['replication_driver_data']) + is_primary = rep_data['is_primary'] + if is_primary: + remote_device_id = ( + self.configuration.replication_device[0]['target_device_id']) + else: + remote_device_id = self._get_volume_metadata(volume)['system'] + if secondary != remote_device_id: + msg = (_('Invalid secondary specified, choose from %s.') + % [remote_device_id]) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + mirror_name = self._construct_mirror_name(volume) + mirror_view = self._get_mirror_view(volume) + remote_client = mirror_view._secondary_client + if is_primary: + new_host = ( + self.configuration.replication_device[0][ + 'managed_backend_name']) + else: + new_host = self._get_volume_metadata(volume)['host'] + + rep_data.update({'is_primary': not is_primary}) + # Transfer ownership to secondary and + # update provider_location field + provider_location = volume['provider_location'] + provider_location = self._update_provider_location( + provider_location, + 'system', remote_client.get_array_serial()['array_serial']) + self.get_array_serial() + provider_location = self._update_provider_location( + provider_location, + 'id', + six.text_type(remote_client.get_lun_by_name(volume.name)['lun_id']) + ) + + mirror_view.promote_image(mirror_name) + model_update = {'host': new_host, + 'replication_driver_data': json.dumps(rep_data), + 'provider_location': provider_location} + return model_update + + def get_replication_updates(self, context): + """Return nothing since Cinder doesn't do anything.""" + return [] + + def list_replication_targets(self, context, volume): + """Provides replication target(s) for a volume.""" + targets = {'volume_id': volume.id, 'targets': []} + rep_data = json.loads(volume['replication_driver_data']) + is_primary = rep_data['is_primary'] + if is_primary: + remote_device_id = ( + self.configuration.replication_device[0]['target_device_id']) + else: + remote_device_id = self._get_volume_metadata(volume)['system'] + + targets['targets'] = [{'type': 'managed', + 'target_device_id': remote_device_id}] + return targets + + def _is_replication_enabled(self, volume): + """Return True if replication extra specs is specified. + + If replication_enabled exists and value is ' True', return True. + If replication exists and value is 'True', return True. + Otherwise, return False. + """ + specs = self.get_volumetype_extraspecs(volume) + return specs and specs.get('replication_enabled') == ' True' + + def setup_lun_replication(self, volume, primary_lun_id, + provisioning, tiering): + """Setup replication for LUN, this only happens in primary system.""" + rep_update = {'replication_driver_data': None, + 'replication_status': 'disabled'} + metadata_update = {} + if self._is_replication_enabled(volume): + LOG.debug('Starting setup replication ' + 'for volume: %s.', volume.id) + lun_size = volume['size'] + mirror_name = self._construct_mirror_name(volume) + self._mirror.create_mirror_workflow( + mirror_name, primary_lun_id, volume.name, lun_size, + provisioning, tiering) + + LOG.info(_LI('Successfully setup replication for %s.'), volume.id) + rep_update.update({'replication_driver_data': + self.__class__._build_replication_driver_data( + self.configuration), + 'replication_status': 'enabled'}) + metadata_update = { + 'host': volume.host, + 'system': self.get_array_serial()} + return rep_update, metadata_update + + def cleanup_lun_replication(self, volume): + if self._is_replication_enabled(volume): + LOG.debug('Starting cleanup replication form volume: ' + '%s.', volume.id) + mirror_name = self._construct_mirror_name(volume) + mirror_view = self._get_mirror_view(volume) + mv = mirror_view.get_image(mirror_name) + if mv: + mirror_view.destroy_mirror_view(mirror_name, volume.name, mv) + + def _get_mirror_view(self, volume): + """Determines where to build a Mirror View operator.""" + if volume['replication_driver_data']: + rep_data = json.loads(volume['replication_driver_data']) + is_primary = rep_data['is_primary'] + else: + is_primary = True + if is_primary: + # if on primary, promote to configured array in conf + mirror_view = self._mirror + else: + # else promote to array according to volume data + mirror_view = self._build_mirror_view(volume) + return mirror_view + + @staticmethod + def _build_replication_driver_data(configuration): + """Builds driver specific data for replication. + + This data will be used by secondary backend to connect + primary device. + """ + driver_data = dict() + driver_data['san_ip'] = configuration.san_ip + driver_data['san_login'] = configuration.san_login + driver_data['san_password'] = configuration.san_password + driver_data['san_secondary_ip'] = configuration.san_secondary_ip + driver_data['storage_vnx_authentication_type'] = ( + configuration.storage_vnx_authentication_type) + driver_data['storage_vnx_security_file_dir'] = ( + configuration.storage_vnx_security_file_dir) + driver_data['is_primary'] = True + return json.dumps(driver_data) + + def _build_mirror_view(self, volume=None): + """Builds a client for remote storage device. + + Currently, only support one remote, managed device. + :param volume: if volume is not None, then build a remote client + from volume's replication_driver_data. + """ + configuration = self.configuration + remote_info = None + if volume: + if volume['replication_driver_data']: + remote_info = json.loads(volume['replication_driver_data']) + else: + LOG.warning( + _LW('No replication info from this volume: %s.'), + volume.id) + return None + else: + if not configuration.replication_device: + LOG.info(_LI('Replication is not configured on backend: %s.'), + configuration.config_group) + return None + remote_info = configuration.replication_device[0] + + pool_name = None + managed_backend_name = remote_info.get('managed_backend_name') + if managed_backend_name: + pool_name = vol_utils.extract_host(managed_backend_name, 'pool') + # Copy info to replica configuration for remote client + replica_conf = copy.copy(configuration) + for key in self.REPLICATION_KEYS: + if key in remote_info: + config.Configuration.__setattr__(replica_conf, + key, remote_info[key]) + _remote_client = CommandLineHelper(replica_conf) + _mirror = MirrorView(self._client, _remote_client, pool_name) + return _mirror + def get_pool(self, volume): """Returns the pool name of a volume.""" @@ -3997,10 +4264,10 @@ class EMCVnxCliBase(object): AttachSnapTask(name="AttachSnapTask%s" % i, inject=sub_store_spec), CreateDestLunTask(name="CreateDestLunTask%s" % i, - providers=lun_data_key_template % i, + provides=lun_data_key_template % i, inject=sub_store_spec), MigrateLunTask(name="MigrateLunTask%s" % i, - providers=lun_id_key_template % i, + provides=lun_id_key_template % i, inject=sub_store_spec, rebind={'lun_data': lun_data_key_template % i}, wait_for_completion=False)) @@ -4158,9 +4425,9 @@ class CreateDestLunTask(task.Task): Reversion strategy: Delete the temp destination lun. """ - def __init__(self, name=None, providers='lun_data', inject=None): + def __init__(self, name=None, provides='lun_data', inject=None): super(CreateDestLunTask, self).__init__(name=name, - provides=providers, + provides=provides, inject=inject) def execute(self, client, pool_name, dest_vol_name, volume_size, @@ -4188,10 +4455,10 @@ class MigrateLunTask(task.Task): Reversion strategy: None """ - def __init__(self, name=None, providers='new_lun_id', inject=None, + def __init__(self, name=None, provides='new_lun_id', inject=None, rebind=None, wait_for_completion=True): super(MigrateLunTask, self).__init__(name=name, - provides=providers, + provides=provides, inject=inject, rebind=rebind) self.wait_for_completion = wait_for_completion @@ -4319,3 +4586,356 @@ class WaitMigrationsCompleteTask(task.Task): if not migrated: msg = _("Migrate volume %(src)s failed.") % {'src': lun_id} raise exception.VolumeBackendAPIException(data=msg) + + +class MirrorView(object): + """MirrorView synchronous/asynchronous operations. + + This class is to support operations for volume replication. + Each operation should ensure commands are sent to correct targeting device. + + NOTE: currently, only synchronous is supported. + """ + SYNCHRONIZE_MODE = ['sync'] + + SYNCHRONIZED_STATE = 'Synchronized' + CONSISTENT_STATE = 'Consistent' + + def __init__(self, client, secondary_client, pool_name, mode='sync'): + """Caller needs to initialize MirrorView via this method. + + :param client: client connecting to primary system + :param secondary_client: client connecting to secondary system + :param mode: only 'sync' is allowed + """ + self._client = client + self._secondary_client = secondary_client + self.pool_name = pool_name + if mode not in self.SYNCHRONIZE_MODE: + msg = _('Invalid synchronize mode specified, allowed ' + 'mode is %s.') % self.SYNCHRONIZE_MODE + raise exception.VolumeBackendAPIException( + data=msg) + self.mode = '-sync' + + def create_mirror_workflow(self, mirror_name, lun_id, + lun_name, lun_size, provisioning, tiering): + """Creates mirror view for LUN.""" + store_spec = {'mirror': self} + work_flow = self._get_create_mirror_flow( + mirror_name, lun_id, lun_name, lun_size, provisioning, tiering) + flow_engine = taskflow.engines.load(work_flow, store=store_spec) + flow_engine.run() + + def destroy_mirror_view(self, mirror_name, lun_name, mv=None): + self.fracture_image(mirror_name, mv) + self.remove_image(mirror_name, mv) + self.destroy_mirror(mirror_name) + self.delete_secondary_lun(lun_name) + + def _get_create_mirror_flow(self, mirror_name, lun_id, + lun_name, lun_size, provisioning, tiering): + """Gets mirror create flow.""" + flow_name = 'create_mirror_view' + work_flow = linear_flow.Flow(flow_name) + work_flow.add(MirrorCreateTask(mirror_name, lun_id), + MirrorSecLunCreateTask(lun_name, lun_size, + provisioning, tiering), + MirrorAddImageTask(mirror_name)) + return work_flow + + def create_mirror(self, name, primary_lun_id, poll=False): + command_create = ('mirror', '-sync', '-create', + '-name', name, '-lun', primary_lun_id, + '-usewriteintentlog', '-o') + out, rc = self._client.command_execute(*command_create, poll=poll) + if rc != 0 or out.strip(): + if VNXError.has_error(out, VNXError.MIRROR_IN_USE): + LOG.warning(_LW('MirrorView already created, mirror name ' + '%(name)s. Message: %(msg)s'), + {'name': name, 'msg': out}) + else: + self._client._raise_cli_error(cmd=command_create, + rc=rc, + out=out) + return rc + + def create_secondary_lun(self, lun_name, lun_size, provisioning, + tiering, poll=False): + """Creates secondary LUN in remote device.""" + data = self._secondary_client.create_lun_with_advance_feature( + pool=self.pool_name, + name=lun_name, + size=lun_size, + provisioning=provisioning, + tiering=tiering, + consistencygroup_id=None, + ignore_thresholds=False, + poll=poll) + return data['lun_id'] + + def delete_secondary_lun(self, lun_name): + """Deletes secondary LUN in remote device.""" + self._secondary_client.delete_lun(lun_name) + + def destroy_mirror(self, name, poll=False): + command_destroy = ('mirror', '-sync', '-destroy', + '-name', name, '-force', '-o') + out, rc = self._client.command_execute(*command_destroy, poll=poll) + if rc != 0 or out.strip(): + if VNXError.has_error(out, VNXError.MIRROR_NOT_FOUND): + LOG.warning(_LW('MirrorView %(name)s was already deleted. ' + 'Message: %(msg)s'), + {'name': name, 'msg': out}) + else: + self._client._raise_cli_error(cmd=command_destroy, + rc=rc, + out=out) + return out, rc + + def add_image(self, name, secondary_lun_id, poll=False): + """Adds secondary image to mirror.""" + secondary_array_ip = self._secondary_client.active_storage_ip + command_add = ('mirror', '-sync', '-addimage', + '-name', name, '-arrayhost', secondary_array_ip, + '-lun', secondary_lun_id, '-recoverypolicy', 'auto', + '-syncrate', 'high') + out, rc = self._client.command_execute(*command_add, poll=poll) + if rc != 0: + self._client._raise_cli_error(cmd=command_add, + rc=rc, + out=out) + return out, rc + + def remove_image(self, name, mirror_view=None, poll=False): + """Removes secondary image(s) from mirror.""" + if not mirror_view: + mirror_view = self.get_image(name, poll=True) + image_uid = self._extract_image_uid(mirror_view, 'secondary') + command_remove = ('mirror', '-sync', '-removeimage', + '-name', name, '-imageuid', image_uid, + '-o') + out, rc = self._client.command_execute(*command_remove, poll=poll) + if rc != 0: + self._client._raise_cli_error(cmd=command_remove, + rc=rc, + out=out) + return out, rc + + def get_image(self, name, use_secondary=False, poll=False): + """Returns mirror view properties. + + :param name: mirror view name + :param use_secondary: get image info from secodnary or not + :return: dict of mirror view properties as below: + { + 'MirrorView Name': 'mirror name', + 'MirrorView Description': 'some desciption here', + ..., + 'images': [ + { + 'Image UID': '50:06:01:60:88:60:08:0F', + 'Is Image Primary': 'YES', + ... + 'Preferred SP': 'A' + }, + { + 'Image UID': '50:06:01:60:88:60:03:BA', + 'Is Image Primary': 'NO', + ..., + 'Synchronizing Progress(%)': 100 + } + ] + } + """ + if use_secondary: + client = self._secondary_client + else: + client = self._client + command_get = ('mirror', '-sync', '-list', '-name', name) + out, rc = client.command_execute( + *command_get, poll=poll) + if rc != 0: + if VNXError.has_error(out, VNXError.MIRROR_NOT_FOUND): + LOG.warning(_LW('Getting MirrorView %(name)s failed.' + ' Message: %(msg)s.'), + {'name': name, 'msg': out}) + return None + else: + client._raise_cli_error(cmd=command_get, + rc=rc, + out=out) + mvs = {} + mvs_info, images_info = re.split('Images:\s*', out) + for line in mvs_info.strip('\n').split('\n'): + k, v = re.split(':\s*', line, 1) + mvs[k] = v if not v or re.search('\D', v) else int(v) + mvs['images'] = [] + for image_raw in re.split('\n\n+', images_info.strip('\n')): + image = {} + for line in image_raw.split('\n'): + k, v = re.split(':\s*', line, 1) + image[k] = v if not v or re.search('\D', v) else int(v) + mvs['images'].append(image) + return mvs + + def fracture_image(self, name, mirror_view=None, poll=False): + """Stops the synchronization between LUNs.""" + if not mirror_view: + mirror_view = self.get_image(name, poll=True) + image_uid = self._extract_image_uid(mirror_view, 'secondary') + command_fracture = ('mirror', '-sync', '-fractureimage', '-name', name, + '-imageuid', image_uid, '-o') + out, rc = self._client.command_execute(*command_fracture, poll=poll) + if rc != 0: + self._client._raise_cli_error(cmd=command_fracture, + rc=rc, + out=out) + return out, rc + + def sync_image(self, name, mirror_view=None, poll=False): + """Synchronizes the secondary image and wait for completion.""" + if not mirror_view: + mirror_view = self.get_image(name, poll=True) + image_state = mirror_view['images'][1]['Image State'] + if image_state == self.SYNCHRONIZED_STATE: + LOG.debug('replication %(name)s is already in %(state)s.', + {'name': name, 'state': image_state}) + return "", 0 + image_uid = self._extract_image_uid(mirror_view, 'secondary') + command_sync = ('mirror', '-sync', '-syncimage', '-name', name, + '-imageuid', image_uid, '-o') + out, rc = self._client.command_execute(*command_sync, poll=poll) + if rc != 0: + self._client._raise_cli_error(cmd=command_sync, + rc=rc, + out=out) + + def inner(): + tmp_mirror = self.get_image(name) + for image in tmp_mirror['images']: + if 'Image State' in image: + # Only secondary image contains this field + return (image['Image State'] == self.SYNCHRONIZED_STATE and + image['Synchronizing Progress(%)'] == 100) + self._client._wait_for_a_condition(inner) + return out, rc + + def promote_image(self, name, mirror_view=None, poll=False): + """Promotes the secondary image on secondary system. + + NOTE: Only when "Image State" in 'Consistent' or 'Synchnonized' + can be promoted. + """ + if not mirror_view: + mirror_view = self.get_image(name, use_secondary=True, poll=True) + image_uid = self._extract_image_uid(mirror_view, 'secondary') + + command_promote = ('mirror', '-sync', '-promoteimage', '-name', name, + '-imageuid', image_uid, '-o') + out, rc = self._secondary_client.command_execute( + *command_promote, poll=poll) + if rc != 0: + raise self._client._raise_cli_error(command_promote, rc, out) + return out, rc + + def _extract_image_uid(self, mirror_view, image_type='primary'): + """Returns primary or secondary image uid from mirror objects. + + :param mirror_view: parsed mirror view. + :param image_type: 'primary' or 'secondary'. + """ + images = mirror_view['images'] + for image in images: + is_primary = image['Is Image Primary'] + if image_type == 'primary' and is_primary == 'YES': + image_uid = image['Image UID'] + break + if image_type == 'secondary' and is_primary == 'NO': + image_uid = image['Image UID'] + break + return image_uid + + +class MirrorCreateTask(task.Task): + """Creates a MirrorView with primary lun for replication. + + Reversion strategy: Destroy the created MirrorView. + """ + def __init__(self, mirror_name, primary_lun_id, **kwargs): + super(MirrorCreateTask, self).__init__() + self.mirror_name = mirror_name + self.primary_lun_id = primary_lun_id + + def execute(self, mirror, *args, **kwargs): + LOG.debug('%s.execute', self.__class__.__name__) + mirror.create_mirror(self.mirror_name, self.primary_lun_id, poll=True) + + def revert(self, result, mirror, *args, **kwargs): + method_name = '%s.revert' % self.__class__.__name__ + LOG.debug(method_name) + if isinstance(result, failure.Failure): + return + else: + LOG.warning(_LW('%(method)s: destroying mirror ' + 'view %(name)s.'), + {'method': method_name, + 'name': self.mirror_name}) + mirror.destroy_mirror(self.mirror_name, poll=True) + + +class MirrorSecLunCreateTask(task.Task): + """Creates a secondary LUN on secondary system. + + Reversion strategy: Delete secondary LUN. + """ + def __init__(self, lun_name, lun_size, provisioning, tiering): + super(MirrorSecLunCreateTask, self).__init__(provides='sec_lun_id') + self.lun_name = lun_name + self.lun_size = lun_size + self.provisioning = provisioning + self.tiering = tiering + + def execute(self, mirror, *args, **kwargs): + LOG.debug('%s.execute', self.__class__.__name__) + sec_lun_id = mirror.create_secondary_lun( + self.lun_name, self.lun_size, self.provisioning, self.tiering) + return sec_lun_id + + def revert(self, result, mirror, *args, **kwargs): + method_name = '%s.revert' % self.__class__.__name__ + LOG.debug(method_name) + if isinstance(result, failure.Failure): + return + else: + LOG.warning(_LW('%(method)s: destroying secondary LUN ' + '%(name)s.'), + {'method': method_name, + 'name': self.lun_name}) + mirror.delete_secondary_lun(self.lun_name) + + +class MirrorAddImageTask(task.Task): + """Add the secondary image to MirrorView. + + Reversion strategy: Remove the secondary image. + """ + def __init__(self, mirror_name): + super(MirrorAddImageTask, self).__init__() + self.mirror_name = mirror_name + + def execute(self, mirror, sec_lun_id, *args, **kwargs): + LOG.debug('%s.execute', self.__class__.__name__) + mirror.add_image(self.mirror_name, sec_lun_id, poll=True) + + def revert(self, result, mirror, *args, **kwargs): + method_name = '%s.revert' % self.__class__.__name__ + LOG.debug(method_name) + if isinstance(result, failure.Failure): + return + else: + LOG.warning(_LW('%(method)s: removing secondary image ' + 'from %(name)s.'), + {'method': method_name, + 'name': self.mirror_name}) + mirror.remove_image(self.mirror_name, poll=True) diff --git a/releasenotes/notes/vnx-replication-v2-2afc4ac0c2ecfa60.yaml b/releasenotes/notes/vnx-replication-v2-2afc4ac0c2ecfa60.yaml new file mode 100644 index 000000000..faa792596 --- /dev/null +++ b/releasenotes/notes/vnx-replication-v2-2afc4ac0c2ecfa60.yaml @@ -0,0 +1,3 @@ +--- +features: + - Replication v2 has been added in VNX Cinder driver -- 2.45.2