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:<vnx serial number>, managed_backend_name: <host>@<backend_name>#<pool_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
This commit is contained in:
parent
88c0dad3ac
commit
f17984af10
@ -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':
|
||||
'<is> 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': '<is> 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': '<is> 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': '<is> 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': '<is> 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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 '<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') == '<is> 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)
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Replication v2 has been added in VNX Cinder driver
|
Loading…
Reference in New Issue
Block a user