diff --git a/cinder/tests/unit/test_huawei_drivers.py b/cinder/tests/unit/test_huawei_drivers.py index f68f8ae4c10..6f2bef9fe45 100644 --- a/cinder/tests/unit/test_huawei_drivers.py +++ b/cinder/tests/unit/test_huawei_drivers.py @@ -31,6 +31,7 @@ from cinder.volume.drivers.huawei import fc_zone_helper from cinder.volume.drivers.huawei import huawei_conf from cinder.volume.drivers.huawei import huawei_driver from cinder.volume.drivers.huawei import hypermetro +from cinder.volume.drivers.huawei import replication from cinder.volume.drivers.huawei import rest_client from cinder.volume.drivers.huawei import smartx @@ -98,6 +99,31 @@ hyper_volume = {'name': 'volume-21ec7341-9256-497b-97d9-ef48edcf0635', 'value': '11'}], } +sync_replica_specs = {'replication_enabled': ' True', + 'replication_type': ' sync'} +async_replica_specs = {'replication_enabled': ' True', + 'replication_type': ' async'} + +TEST_PAIR_ID = "3400a30d844d0004" +replication_volume = { + 'name': 'volume-21ec7341-9256-497b-97d9-ef48edcf0635', + 'size': 2, + 'volume_name': 'vol1', + 'id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'volume_id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'provider_auth': None, + 'project_id': 'project', + 'display_name': 'vol1', + 'display_description': 'test volume', + 'volume_type_id': None, + 'host': 'ubuntu@huawei#OpenStack_Pool', + 'provider_location': '11', + 'metadata': {'lun_wwn': '6643e8c1004c5f6723e9f454003'}, + 'replication_status': 'disabled', + 'replication_driver_data': + '{"pair_id": "%s", "rmt_lun_id": "1"}' % TEST_PAIR_ID, +} + test_snap = {'name': 'volume-21ec7341-9256-497b-97d9-ef48edcf0635', 'size': 1, 'volume_name': 'vol1', @@ -157,6 +183,22 @@ test_new_type = { 'description': None, } +test_new_replication_type = { + 'name': u'new_type', + 'qos_specs_id': None, + 'deleted': False, + 'created_at': None, + 'updated_at': None, + 'extra_specs': { + 'replication_enabled': ' True', + 'replication_type': ' sync', + }, + 'is_public': True, + 'deleted_at': None, + 'id': u'530a56e1-a1a4-49f3-ab6c-779a6e5d999f', + 'description': None, +} + hypermetro_devices = """ { "remote_device": { @@ -279,7 +321,13 @@ FAKE_LUN_INFO_RESPONSE = """ }, "data": { "ID": "1", - "NAME": "5mFHcBv4RkCcD+JyrWc0SA" + "NAME": "5mFHcBv4RkCcD+JyrWc0SA", + "WWN": "6643e8c1004c5f6723e9f454003", + "DESCRIPTION": "21ec7341-9256-497b-97d9-ef48edcf0635", + "HEALTHSTATUS": "1", + "RUNNINGSTATUS": "27", + "ALLOCTYPE": "1", + "CAPACITY": "2097152" } } """ @@ -294,7 +342,7 @@ FAKE_LUN_GET_SUCCESS_RESPONSE = """ "IOCLASSID": "11", "NAME": "5mFHcBv4RkCcD+JyrWc0SA", "DESCRIPTION": "21ec7341-9256-497b-97d9-ef48edcf0635", - "RUNNINGSTATUS": "2", + "RUNNINGSTATUS": "10", "HEALTHSTATUS": "1", "RUNNINGSTATUS": "27", "LUNLIST": "", @@ -309,7 +357,9 @@ FAKE_LUN_GET_SUCCESS_RESPONSE = """ "WRITECACHEPOLICY": "5", "OWNINGCONTROLLER": "0B", "SMARTCACHEPARTITIONID": "", - "CACHEPARTITIONID": "" + "CACHEPARTITIONID": "", + "WWN": "6643e8c1004c5f6723e9f454003", + "PARENTNAME": "OpenStack_Pool" } } """ @@ -938,7 +988,8 @@ FAKE_SYSTEM_VERSION_RESPONSE = """ "code": 0 }, "data":{ - "PRODUCTVERSION": "V100R001C10" + "PRODUCTVERSION": "V100R001C10", + "wwn": "21003400a30d844d" } } """ @@ -1603,6 +1654,148 @@ MAP_COMMAND_TO_FAKE_RESPONSE['/fc_port/associate?TYPE=213&ASSOCIATEOBJTYPE=' FAKE_PORTS_IN_PG_RESPONSE) +# Replication response +FAKE_GET_REMOTEDEV_RESPONSE = """ +{ + "data":[{ + "ARRAYTYPE":"1", + "HEALTHSTATUS":"1", + "ID":"0", + "NAME":"Huawei.Storage", + "RUNNINGSTATUS":"1", + "WWN":"21003400a30d844d" + }], + "error":{ + "code":0, + "description":"0" + } +} +""" +MAP_COMMAND_TO_FAKE_RESPONSE['/remote_device/GET'] = ( + FAKE_GET_REMOTEDEV_RESPONSE) + +FAKE_CREATE_PAIR_RESPONSE = """ +{ + "data":{ + "ID":"%s" + }, + "error":{ + "code":0, + "description":"0" + } +} +""" % TEST_PAIR_ID +MAP_COMMAND_TO_FAKE_RESPONSE['/REPLICATIONPAIR/POST'] = ( + FAKE_CREATE_PAIR_RESPONSE) + +FAKE_DELETE_PAIR_RESPONSE = """ +{ + "data":{}, + "error":{ + "code":0, + "description":"0" + } +} +""" +MAP_COMMAND_TO_FAKE_RESPONSE['/REPLICATIONPAIR/%s/DELETE' % TEST_PAIR_ID] = ( + FAKE_DELETE_PAIR_RESPONSE) + +FAKE_SET_PAIR_ACCESS_RESPONSE = """ +{ + "data":{}, + "error":{ + "code":0, + "description":"0" + } +} +""" +MAP_COMMAND_TO_FAKE_RESPONSE['/REPLICATIONPAIR/%s/PUT' % TEST_PAIR_ID] = ( + FAKE_SET_PAIR_ACCESS_RESPONSE) + +FAKE_GET_PAIR_NORMAL_RESPONSE = """ +{ + "data":{ + "REPLICATIONMODEL": "1", + "RUNNINGSTATUS": "1", + "SECRESACCESS": "2", + "HEALTHSTATUS": "1", + "ISPRIMARY": "true" + }, + "error":{ + "code":0, + "description":"0" + } +} +""" + +FAKE_GET_PAIR_SPLIT_RESPONSE = """ +{ + "data":{ + "REPLICATIONMODEL": "1", + "RUNNINGSTATUS": "26", + "SECRESACCESS": "2", + "ISPRIMARY": "true" + }, + "error":{ + "code":0, + "description":"0" + } +} +""" + +FAKE_GET_PAIR_SYNC_RESPONSE = """ +{ + "data":{ + "REPLICATIONMODEL": "1", + "RUNNINGSTATUS": "23", + "SECRESACCESS": "2" + }, + "error":{ + "code":0, + "description":"0" + } +} +""" +MAP_COMMAND_TO_FAKE_RESPONSE['/REPLICATIONPAIR/%s/GET' % TEST_PAIR_ID] = ( + FAKE_GET_PAIR_NORMAL_RESPONSE) + +FAKE_SYNC_PAIR_RESPONSE = """ +{ + "data":{}, + "error":{ + "code":0, + "description":"0" + } +} +""" +MAP_COMMAND_TO_FAKE_RESPONSE['/REPLICATIONPAIR/sync/PUT'] = ( + FAKE_SYNC_PAIR_RESPONSE) + +FAKE_SPLIT_PAIR_RESPONSE = """ +{ + "data":{}, + "error":{ + "code":0, + "description":"0" + } +} +""" +MAP_COMMAND_TO_FAKE_RESPONSE['/REPLICATIONPAIR/split/PUT'] = ( + FAKE_SPLIT_PAIR_RESPONSE) + +FAKE_SWITCH_PAIR_RESPONSE = """ +{ + "data":{}, + "error":{ + "code":0, + "description":"0" + } +} +""" +MAP_COMMAND_TO_FAKE_RESPONSE['/REPLICATIONPAIR/switch/PUT'] = ( + FAKE_SWITCH_PAIR_RESPONSE) + + def Fake_sleep(time): pass @@ -1612,7 +1805,14 @@ class FakeHuaweiConf(object): self.conf = conf self.protocol = protocol + def safe_get(self, key): + try: + return getattr(self.conf, key) + except Exception: + return + def update_config_value(self): + setattr(self.conf, 'volume_backend_name', 'huawei_storage') setattr(self.conf, 'san_address', ['http://100.115.10.69:8082/deviceManager/rest/']) setattr(self.conf, 'san_user', 'admin') @@ -1646,6 +1846,16 @@ class FakeHuaweiConf(object): 'TargetPortGroup': 'portgroup-test', } setattr(self.conf, 'iscsi_info', [iscsi_info]) + targets = [{'target_device_id': 'huawei-replica-1', + 'managed_backend_name': 'ubuntu@huawei2#OpenStack_Pool', + 'san_address': + 'https://100.97.10.69:8088/deviceManager/rest/', + 'san_user': 'admin', + 'san_password': 'Admin@storage1'}] + setattr(self.conf, 'replication_device', targets) + + setattr(self.conf, 'safe_get', self.safe_get) + class FakeClient(rest_client.RestClient): @@ -1717,6 +1927,11 @@ class FakeDB(object): return volumes +class FakeReplicaPairManager(replication.ReplicaPairManager): + def _init_rmt_client(self): + self.rmt_client = FakeClient(self.conf) + + class FakeISCSIStorage(huawei_driver.HuaweiISCSIDriver): """Fake Huawei Storage, Rewrite some methods of HuaweiISCSIDriver.""" @@ -1734,6 +1949,7 @@ class FakeISCSIStorage(huawei_driver.HuaweiISCSIDriver): self.rmt_client, self.configuration, self.db) + self.replica = FakeReplicaPairManager(self.client, self.configuration) class FakeFCStorage(huawei_driver.HuaweiFCDriver): @@ -1754,6 +1970,7 @@ class FakeFCStorage(huawei_driver.HuaweiFCDriver): self.rmt_client, self.configuration, self.db) + self.replica = FakeReplicaPairManager(self.client, self.configuration) @ddt.ddt @@ -1836,9 +2053,20 @@ class HuaweiISCSIDriverTestCase(test.TestCase): self.assertTrue(delete_flag) def test_create_volume_from_snapsuccess(self): - lun_info = self.driver.create_volume_from_snapshot(test_volume, - test_volume) - self.assertEqual('1', lun_info['provider_location']) + self.mock_object( + huawei_driver.HuaweiBaseDriver, + '_get_volume_type', + mock.Mock(return_value={'extra_specs': sync_replica_specs})) + self.mock_object(replication.ReplicaCommonDriver, 'sync') + model_update = self.driver.create_volume_from_snapshot(test_volume, + test_volume) + self.assertEqual('1', model_update['provider_location']) + + driver_data = {'pair_id': TEST_PAIR_ID, + 'rmt_lun_id': '1'} + driver_data = replication.to_string(driver_data) + self.assertEqual(driver_data, model_update['replication_driver_data']) + self.assertEqual('enabled', model_update['replication_status']) def test_initialize_connection_success(self): iscsi_properties = self.driver.initialize_connection(test_volume, @@ -1850,7 +2078,7 @@ class HuaweiISCSIDriverTestCase(test.TestCase): def test_get_volume_status(self): data = self.driver.get_volume_stats() - self.assertEqual('2.0.3', data['driver_version']) + self.assertEqual('2.0.5', data['driver_version']) def test_extend_volume(self): @@ -2501,6 +2729,311 @@ class HuaweiISCSIDriverTestCase(test.TestCase): self.driver.unmanage_snapshot(test_snapshot) self.assertEqual(1, mock_rename.call_count) + def test_init_rmt_client(self): + self.mock_object(rest_client, 'RestClient', + mock.Mock(return_value=None)) + replica = replication.ReplicaPairManager(self.driver.client, + self.configuration) + self.assertEqual(replica.rmt_pool, 'OpenStack_Pool') + self.assertEqual(replica.target_dev_id, 'huawei-replica-1') + + @ddt.data(sync_replica_specs, async_replica_specs) + def test_create_replication_success(self, mock_type): + self.mock_object(replication.ReplicaCommonDriver, 'sync') + self.mock_object( + huawei_driver.HuaweiBaseDriver, + '_get_volume_type', + mock.Mock(return_value={'extra_specs': mock_type})) + + model_update = self.driver.create_volume(replication_volume) + driver_data = {'pair_id': TEST_PAIR_ID, + 'rmt_lun_id': '1'} + driver_data = replication.to_string(driver_data) + self.assertEqual(driver_data, model_update['replication_driver_data']) + self.assertEqual('enabled', model_update['replication_status']) + + @ddt.data( + [ + rest_client.RestClient, + 'get_array_info', + mock.Mock( + side_effect=exception.VolumeBackendAPIException(data='err')) + ], + [ + rest_client.RestClient, + 'get_remote_devices', + mock.Mock( + side_effect=exception.VolumeBackendAPIException(data='err')) + ], + [ + rest_client.RestClient, + 'get_remote_devices', + mock.Mock(return_value={}) + ], + [ + replication.ReplicaPairManager, + 'wait_volume_online', + mock.Mock(side_effect=[ + None, + exception.VolumeBackendAPIException(data='err')]) + ], + [ + rest_client.RestClient, + 'create_pair', + mock.Mock( + side_effect=exception.VolumeBackendAPIException(data='err')) + ], + [ + replication.ReplicaCommonDriver, + 'sync', + mock.Mock( + side_effect=exception.VolumeBackendAPIException(data='err')) + ], + ) + @ddt.unpack + def test_create_replication_fail(self, mock_module, mock_func, mock_value): + self.mock_object( + huawei_driver.HuaweiBaseDriver, + '_get_volume_type', + mock.Mock(return_value={'extra_specs': sync_replica_specs})) + self.mock_object(replication.ReplicaPairManager, '_delete_pair') + self.mock_object(mock_module, mock_func, mock_value) + self.assertRaises( + exception.VolumeBackendAPIException, + self.driver.create_volume, replication_volume) + + def test_delete_replication_success(self): + self.mock_object(replication.ReplicaCommonDriver, 'split') + self.mock_object( + huawei_driver.HuaweiBaseDriver, + '_get_volume_type', + mock.Mock(return_value={'extra_specs': sync_replica_specs})) + self.driver.delete_volume(replication_volume) + + self.mock_object(rest_client.RestClient, 'check_lun_exist', + mock.Mock(return_value=False)) + self.driver.delete_volume(replication_volume) + + def test_wait_volume_online(self): + replica = FakeReplicaPairManager(self.driver.client, + self.configuration) + lun_info = {'ID': '11'} + + replica.wait_volume_online(self.driver.client, lun_info) + + offline_status = {'RUNNINGSTATUS': '28'} + replica.wait_volume_online(self.driver.client, lun_info) + + with mock.patch.object(rest_client.RestClient, 'get_lun_info', + offline_status): + self.assertRaises(exception.VolumeBackendAPIException, + replica.wait_volume_online, + self.driver.client, + lun_info) + + def test_wait_second_access(self): + pair_id = '1' + access_ro = constants.REPLICA_SECOND_RO + access_rw = constants.REPLICA_SECOND_RW + op = replication.PairOp(self.driver.client) + common_driver = replication.ReplicaCommonDriver(self.configuration, op) + self.mock_object(replication.PairOp, 'get_replica_info', + mock.Mock(return_value={'SECRESACCESS': access_ro})) + + common_driver.wait_second_access(pair_id, access_ro) + self.assertRaises(exception.VolumeBackendAPIException, + common_driver.wait_second_access, pair_id, access_rw) + + def test_wait_replica_ready(self): + normal_status = { + 'RUNNINGSTATUS': constants.REPLICA_RUNNING_STATUS_NORMAL, + 'HEALTHSTATUS': constants.REPLICA_HEALTH_STATUS_NORMAL + } + split_status = { + 'RUNNINGSTATUS': constants.REPLICA_RUNNING_STATUS_SPLIT, + 'HEALTHSTATUS': constants.REPLICA_HEALTH_STATUS_NORMAL + } + sync_status = { + 'RUNNINGSTATUS': constants.REPLICA_RUNNING_STATUS_SYNC, + 'HEALTHSTATUS': constants.REPLICA_HEALTH_STATUS_NORMAL + } + pair_id = '1' + op = replication.PairOp(self.driver.client) + common_driver = replication.ReplicaCommonDriver(self.configuration, op) + with mock.patch.object(replication.PairOp, 'get_replica_info', + mock.Mock(return_value=normal_status)): + common_driver.wait_replica_ready(pair_id) + + with mock.patch.object( + replication.PairOp, + 'get_replica_info', + mock.Mock(side_effect=[sync_status, normal_status])): + common_driver.wait_replica_ready(pair_id) + + with mock.patch.object(replication.PairOp, 'get_replica_info', + mock.Mock(return_value=split_status)): + self.assertRaises(exception.VolumeBackendAPIException, + common_driver.wait_replica_ready, pair_id) + + def test_replication_enable_success(self): + self.mock_object(replication.ReplicaCommonDriver, 'unprotect_second') + self.mock_object(replication.ReplicaCommonDriver, 'split') + self.mock_object(replication.PairOp, 'is_primary', + mock.Mock(side_effect=[False, True])) + self.driver.replication_enable(None, replication_volume) + + @ddt.data( + [ + replication.AbsReplicaOp, + 'is_running_status', + mock.Mock(return_value=False) + ], + [ + replication, + 'get_replication_driver_data', + mock.Mock(return_value={}) + ], + [ + replication.PairOp, + 'get_replica_info', + mock.Mock(return_value={}) + ], + ) + @ddt.unpack + def test_replication_enable_fail(self, mock_module, mock_func, mock_value): + self.mock_object(mock_module, mock_func, mock_value) + self.assertRaises( + exception.VolumeBackendAPIException, + self.driver.replication_enable, None, replication_volume) + + def test_replication_disable_fail(self): + self.assertRaises( + exception.VolumeBackendAPIException, + self.driver.replication_disable, None, replication_volume) + + def test_replication_disable_success(self): + self.mock_object(replication.ReplicaCommonDriver, 'split') + self.driver.replication_disable(None, replication_volume) + + self.mock_object(replication, 'get_replication_driver_data', + mock.Mock(return_value={})) + self.driver.replication_disable(None, replication_volume) + + def test_replication_failover_success(self): + self.mock_object(replication.ReplicaCommonDriver, 'split') + self.mock_object(replication.PairOp, 'is_primary', + mock.Mock(return_value=False)) + model_update = self.driver.replication_failover( + None, replication_volume, None) + self.assertEqual('ubuntu@huawei2#OpenStack_Pool', model_update['host']) + self.assertEqual('1', model_update['provider_location']) + driver_data = {'pair_id': TEST_PAIR_ID, + 'rmt_lun_id': '11'} + driver_data = replication.to_string(driver_data) + self.assertEqual(driver_data, model_update['replication_driver_data']) + + @ddt.data( + [ + replication.PairOp, + 'is_primary', + mock.Mock(return_value=True) + ], + [ + replication.PairOp, + 'is_primary', + mock.Mock(return_value=False) + ], + [ + replication, + 'get_replication_driver_data', + mock.Mock(return_value={}) + ], + [ + replication, + 'get_replication_driver_data', + mock.Mock(return_value={'pair_id': '1'}) + ], + ) + @ddt.unpack + def test_replication_failover_fail(self, + mock_module, mock_func, mock_value): + self.mock_object( + replication.ReplicaCommonDriver, + 'wait_second_access', + mock.Mock( + side_effect=exception.VolumeBackendAPIException(data="error"))) + self.mock_object(mock_module, mock_func, mock_value) + self.assertRaises( + exception.VolumeBackendAPIException, + self.driver.replication_failover, + None, + replication_volume, None) + + def test_list_replication_targets(self): + info = self.driver.list_replication_targets(None, replication_volume) + targets = [{'target_device_id': 'huawei-replica-1'}] + self.assertEqual(targets, info['targets']) + + self.mock_object(replication, 'get_replication_driver_data', + mock.Mock(return_value={})) + info = self.driver.list_replication_targets(None, replication_volume) + self.assertEqual(targets, info['targets']) + + @ddt.data(constants.REPLICA_SECOND_RW, constants.REPLICA_SECOND_RO) + def test_replication_protect_second(self, mock_access): + replica_id = TEST_PAIR_ID + op = replication.PairOp(self.driver.client) + common_driver = replication.ReplicaCommonDriver(self.configuration, op) + + self.mock_object(replication.ReplicaCommonDriver, 'wait_second_access') + self.mock_object( + replication.PairOp, + 'get_replica_info', + mock.Mock(return_value={'SECRESACCESS': mock_access})) + + common_driver.protect_second(replica_id) + common_driver.unprotect_second(replica_id) + + def test_replication_sync(self): + replica_id = TEST_PAIR_ID + op = replication.PairOp(self.driver.client) + common_driver = replication.ReplicaCommonDriver(self.configuration, op) + async_normal_status = { + 'REPLICATIONMODEL': constants.REPLICA_ASYNC_MODEL, + 'RUNNINGSTATUS': constants.REPLICA_RUNNING_STATUS_NORMAL, + 'HEALTHSTATUS': constants.REPLICA_HEALTH_STATUS_NORMAL + } + + self.mock_object(replication.ReplicaCommonDriver, 'protect_second') + self.mock_object(replication.PairOp, 'get_replica_info', + mock.Mock(return_value=async_normal_status)) + common_driver.sync(replica_id, True) + common_driver.sync(replica_id, False) + + def test_replication_split(self): + replica_id = TEST_PAIR_ID + op = replication.PairOp(self.driver.client) + common_driver = replication.ReplicaCommonDriver(self.configuration, op) + + self.mock_object(replication.ReplicaCommonDriver, 'wait_expect_state') + self.mock_object(replication.PairOp, 'split', mock.Mock( + side_effect=exception.VolumeBackendAPIException(data='err'))) + common_driver.split(replica_id) + + def test_replication_base_op(self): + replica_id = '1' + op = replication.AbsReplicaOp(None) + op.create() + op.delete(replica_id) + op.protect_second(replica_id) + op.unprotect_second(replica_id) + op.sync(replica_id) + op.split(replica_id) + op.switch(replica_id) + op.is_primary({}) + op.get_replica_info(replica_id) + op._is_status(None, {'key': 'volue'}, None) + class FCSanLookupService(object): @@ -2565,9 +3098,33 @@ class HuaweiFCDriverTestCase(test.TestCase): self.assertTrue(self.driver.client.terminateFlag) def test_get_volume_status(self): - + remote_device_info = {"ARRAYTYPE": "1", + "HEALTHSTATUS": "1", + "RUNNINGSTATUS": "10"} + self.mock_object( + replication.ReplicaPairManager, + 'get_remote_device_by_wwn', + mock.Mock(return_value=remote_device_info)) data = self.driver.get_volume_stats() - self.assertEqual('2.0.4', data['driver_version']) + self.assertEqual('2.0.5', data['driver_version']) + self.assertTrue(data['pools'][0]['replication_enabled']) + self.assertListEqual(['sync', 'async'], + data['pools'][0]['replication_type']) + + self.mock_object( + replication.ReplicaPairManager, + 'get_remote_device_by_wwn', + mock.Mock(return_value={})) + data = self.driver.get_volume_stats() + self.assertNotIn('replication_enabled', data['pools'][0]) + + self.mock_object( + replication.ReplicaPairManager, + 'try_get_remote_wwn', + mock.Mock(return_value={})) + data = self.driver.get_volume_stats() + self.assertEqual('2.0.5', data['driver_version']) + self.assertNotIn('replication_enabled', data['pools'][0]) def test_extend_volume(self): self.driver.extend_volume(test_volume, 3) @@ -2755,6 +3312,17 @@ class HuaweiFCDriverTestCase(test.TestCase): test_new_type, None, test_host) self.assertTrue(retype) + @mock.patch.object(rest_client.RestClient, 'add_lun_to_partition') + @mock.patch.object( + huawei_driver.HuaweiBaseDriver, + '_get_volume_type', + return_value={'extra_specs': sync_replica_specs}) + def test_retype_replication_volume_success(self, mock_get_type, + mock_add_lun_to_partition): + retype = self.driver.retype(None, test_volume, + test_new_replication_type, None, test_host) + self.assertTrue(retype) + def test_retype_volume_cache_fail(self): self.driver.client.cache_not_exist = True diff --git a/cinder/volume/drivers/huawei/constants.py b/cinder/volume/drivers/huawei/constants.py index acd5b3de859..44e76ab8cf5 100644 --- a/cinder/volume/drivers/huawei/constants.py +++ b/cinder/volume/drivers/huawei/constants.py @@ -75,3 +75,31 @@ QOS_KEYS = ['MAXIOPS', 'MINIOPS', 'MINBANDWidth', 'MAXBANDWidth', 'LATENCY', 'IOTYPE'] MAX_LUN_NUM_IN_QOS = 64 HYPERMETRO_CLASS = "cinder.volume.drivers.huawei.hypermetro.HuaweiHyperMetro" + +DEFAULT_REPLICA_WAIT_INTERVAL = 1 +DEFAULT_REPLICA_WAIT_TIMEOUT = 10 + +REPLICA_SYNC_MODEL = '1' +REPLICA_ASYNC_MODEL = '2' +REPLICA_SPEED = '2' +REPLICA_PERIOD = '3600' +REPLICA_SECOND_RO = '2' +REPLICA_SECOND_RW = '3' + +REPLICA_RUNNING_STATUS_KEY = 'RUNNINGSTATUS' +REPLICA_RUNNING_STATUS_INITIAL_SYNC = '21' +REPLICA_RUNNING_STATUS_SYNC = '23' +REPLICA_RUNNING_STATUS_SYNCED = '24' +REPLICA_RUNNING_STATUS_NORMAL = '1' +REPLICA_RUNNING_STATUS_SPLIT = '26' +REPLICA_RUNNING_STATUS_INVALID = '35' + +REPLICA_HEALTH_STATUS_KEY = 'HEALTHSTATUS' +REPLICA_HEALTH_STATUS_NORMAL = '1' + +REPLICA_LOCAL_DATA_STATUS_KEY = 'PRIRESDATASTATUS' +REPLICA_REMOTE_DATA_STATUS_KEY = 'SECRESDATASTATUS' +REPLICA_DATA_SYNC_KEY = 'ISDATASYNC' +REPLICA_DATA_STATUS_SYNCED = '1' +REPLICA_DATA_STATUS_COMPLETE = '2' +REPLICA_DATA_STATUS_INCOMPLETE = '3' diff --git a/cinder/volume/drivers/huawei/huawei_conf.py b/cinder/volume/drivers/huawei/huawei_conf.py index 06a8ef4990f..f4781dee5ce 100644 --- a/cinder/volume/drivers/huawei/huawei_conf.py +++ b/cinder/volume/drivers/huawei/huawei_conf.py @@ -151,7 +151,6 @@ class HuaweiConf(object): else: msg = (_("Invalid lun type %s is configured.") % lun_type) LOG.exception(msg) - raise exception.InvalidInput(reason=msg) setattr(self.conf, 'lun_type', lun_type) diff --git a/cinder/volume/drivers/huawei/huawei_driver.py b/cinder/volume/drivers/huawei/huawei_driver.py index 3431db1001a..06eca13d79a 100644 --- a/cinder/volume/drivers/huawei/huawei_driver.py +++ b/cinder/volume/drivers/huawei/huawei_driver.py @@ -33,6 +33,7 @@ from cinder.volume.drivers.huawei import fc_zone_helper from cinder.volume.drivers.huawei import huawei_conf from cinder.volume.drivers.huawei import huawei_utils from cinder.volume.drivers.huawei import hypermetro +from cinder.volume.drivers.huawei import replication from cinder.volume.drivers.huawei import rest_client from cinder.volume.drivers.huawei import smartx from cinder.volume import utils as volume_utils @@ -90,6 +91,10 @@ class HuaweiBaseDriver(driver.VolumeDriver): metro_san_password) self.rmt_client.login() + # init replication manager + self.replica = replication.ReplicaPairManager(self.client, + self.configuration) + def check_for_setup_error(self): pass @@ -98,7 +103,9 @@ class HuaweiBaseDriver(driver.VolumeDriver): self.huawei_conf.update_config_value() if self.metro_flag: self.rmt_client.get_all_pools() - return self.client.update_volume_stats() + stats = self.client.update_volume_stats() + stats = self.replica.update_replica_capability(stats) + return stats def _get_volume_type(self, volume): volume_type = None @@ -127,6 +134,8 @@ class HuaweiBaseDriver(driver.VolumeDriver): 'thin_provisioning_support': False, 'thick_provisioning_support': False, 'hypermetro': False, + 'replication_enabled': False, + 'replication_type': 'async', } opts_value = { @@ -146,6 +155,7 @@ class HuaweiBaseDriver(driver.VolumeDriver): opts_associate, specs) opts = smartx.SmartX().get_smartx_specs_opts(opts) + opts = replication.get_replication_opts(opts) LOG.debug('volume opts %(opts)s.', {'opts': opts}) return opts @@ -172,12 +182,15 @@ class HuaweiBaseDriver(driver.VolumeDriver): if ((not scope or scope == 'capabilities') and key in opts_capability): words = value.split() - if not (words and len(words) == 2 and words[0] == ''): - LOG.error(_LE("Extra specs must be specified as " - "capabilities:%s=' True' or " - "' true'."), key) - else: + if words and len(words) == 2 and words[0] in ('', ''): opts[key] = words[1].lower() + elif key == 'replication_type': + LOG.error(_LE("Extra specs must be specified as " + "replication_type=' sync' or " + "' async'.")) + else: + LOG.error(_LE("Extra specs must be specified as " + "capabilities:%s=' True'."), key) if ((scope in opts_capability) and (key in opts_value) @@ -193,7 +206,7 @@ class HuaweiBaseDriver(driver.VolumeDriver): 'TYPE': '11', 'NAME': huawei_utils.encode_name(volume['id']), 'PARENTTYPE': '216', - 'PARENTID': self.client.get_pool_id(volume, pool_name), + 'PARENTID': self.client.get_pool_id(pool_name), 'DESCRIPTION': volume['name'], 'ALLOCTYPE': opts.get('LUNType', self.configuration.lun_type), 'CAPACITY': huawei_utils.get_volume_size(volume), @@ -220,10 +233,11 @@ class HuaweiBaseDriver(driver.VolumeDriver): model_update['metadata'] = metadata return lun_info, model_update - def create_volume(self, volume): - """Create a volume.""" - volume_type = self._get_volume_type(volume) - opts = self._get_volume_params(volume_type) + def _create_base_type_volume(self, opts, volume, volume_type): + """Create volume and add some base type. + + Base type is the services won't conflict with the other service. + """ lun_params = self._get_lun_params(volume, opts) lun_info, model_update = self._create_volume(volume, lun_params) lun_id = lun_info['ID'] @@ -244,7 +258,17 @@ class HuaweiBaseDriver(driver.VolumeDriver): msg = _('Create volume error. Because %s.') % six.text_type(err) raise exception.VolumeBackendAPIException(data=msg) - if (opts.get('hypermetro') and opts.get('hypermetro') == 'true'): + return lun_params, lun_info, model_update + + def _add_extend_type_to_volume(self, opts, lun_params, lun_info, + model_update): + """Add the extend type. + + Extend type is the services may conflict with LUNCopy. + So add it after the those services. + """ + lun_id = lun_info['ID'] + if opts.get('hypermetro') == 'true': metro = hypermetro.HuaweiHyperMetro(self.client, self.rmt_client, self.configuration, @@ -257,6 +281,35 @@ class HuaweiBaseDriver(driver.VolumeDriver): self._delete_lun_with_check(lun_id) raise + if opts.get('replication_enabled') == 'true': + replica_model = opts.get('replication_type') + try: + replica_info = self.replica.create_replica(lun_info, + replica_model) + model_update.update(replica_info) + except Exception as err: + LOG.exception(_LE('Create replication volume error.')) + self._delete_lun_with_check(lun_id) + raise + + return model_update + + def create_volume(self, volume): + """Create a volume.""" + volume_type = self._get_volume_type(volume) + opts = self._get_volume_params(volume_type) + if (opts.get('hypermetro') == 'true' + and opts.get('replication_enabled') == 'true'): + err_msg = _("Hypermetro and Replication can not be " + "used in the same volume_type.") + LOG.error(err_msg) + raise exception.VolumeBackendAPIException(data=err_msg) + + lun_params, lun_info, model_update = ( + self._create_base_type_volume(opts, volume, volume_type)) + + model_update = self._add_extend_type_to_volume(opts, lun_params, + lun_info, model_update) return model_update def _delete_volume(self, volume): @@ -281,7 +334,7 @@ class HuaweiBaseDriver(driver.VolumeDriver): lun_id = volume.get('provider_location') if not lun_id or not self.client.check_lun_exist(lun_id): LOG.warning(_LW("Can't find lun %s on the array."), lun_id) - return False + return qos_id = self.client.get_qosid_by_lunid(lun_id) if qos_id: @@ -301,6 +354,16 @@ class HuaweiBaseDriver(driver.VolumeDriver): self._delete_volume(volume) raise + # Delete a replication volume + replica_data = volume.get('replication_driver_data') + if replica_data: + try: + self.replica.delete_replica(volume) + except exception.VolumeBackendAPIException as err: + with excutils.save_and_reraise_exception(): + LOG.exception(_LE("Delete replication error.")) + self._delete_volume(volume) + self._delete_volume(volume) def _delete_lun_with_check(self, lun_id): @@ -414,6 +477,15 @@ class HuaweiBaseDriver(driver.VolumeDriver): def migrate_volume(self, ctxt, volume, host, new_type=None): """Migrate a volume within the same array.""" + + # NOTE(jlc): Replication volume can't migrate. But retype + # can remove replication relationship first then do migrate. + # So don't add this judgement into _check_migration_valid(). + volume_type = self._get_volume_type(volume) + opts = self._get_volume_params(volume_type) + if opts.get('replication_enabled') == 'true': + return (False, None) + return self._migrate_volume(volume, host, new_type) def _check_migration_valid(self, host, volume): @@ -516,6 +588,15 @@ class HuaweiBaseDriver(driver.VolumeDriver): We use LUNcopy to copy a new volume from snapshot. The time needed increases as volume size does. """ + volume_type = self._get_volume_type(volume) + opts = self._get_volume_params(volume_type) + if (opts.get('hypermetro') == 'true' + and opts.get('replication_enabled') == 'true'): + err_msg = _("Hypermetro and Replication can not be " + "used in the same volume_type.") + LOG.error(err_msg) + raise exception.VolumeBackendAPIException(data=err_msg) + snapshotname = huawei_utils.encode_name(snapshot['id']) snapshot_id = snapshot.get('provider_location') if snapshot_id is None: @@ -528,7 +609,9 @@ class HuaweiBaseDriver(driver.VolumeDriver): LOG.error(err_msg) raise exception.VolumeBackendAPIException(data=err_msg) - model_update = self.create_volume(volume) + lun_params, lun_info, model_update = ( + self._create_base_type_volume(opts, volume, volume_type)) + tgt_lun_id = model_update['provider_location'] luncopy_name = huawei_utils.encode_name(volume['id']) LOG.info(_LI( @@ -555,6 +638,10 @@ class HuaweiBaseDriver(driver.VolumeDriver): self._copy_volume(volume, luncopy_name, snapshot_id, tgt_lun_id) + # NOTE(jlc): Actually, we just only support replication here right + # now, not hypermetro. + model_update = self._add_extend_type_to_volume(opts, lun_params, + lun_info, model_update) return model_update def create_cloned_volume(self, volume, src_vref): @@ -598,6 +685,14 @@ class HuaweiBaseDriver(driver.VolumeDriver): LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) + volume_type = self._get_volume_type(volume) + opts = self._get_volume_params(volume_type) + if opts.get('replication_enabled') == 'true': + msg = (_("Can't extend replication volume, volume: %(id)s") % + {"id": volume['id']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + old_size = huawei_utils.get_volume_size(volume) new_size = int(new_size) * units.Gi / 512 volume_name = huawei_utils.encode_name(volume['id']) @@ -670,25 +765,51 @@ class HuaweiBaseDriver(driver.VolumeDriver): migration, change_opts, lun_id = self.determine_changes_when_retype( volume, new_type, host) + model_update = {} + replica_enabled_change = change_opts.get('replication_enabled') + replica_type_change = change_opts.get('replication_type') + if replica_enabled_change and replica_enabled_change[0] == 'true': + try: + self.replica.delete_replica(volume) + model_update.update({'replication_status': 'disabled', + 'replication_driver_data': None}) + except exception.VolumeBackendAPIException: + LOG.exception(_LE('Retype volume error. ' + 'Delete replication failed.')) + return False + try: if migration: LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": lun_id, "change_opts": change_opts}) - if self._migrate_volume(volume, host, new_type): - return True - else: + if not self._migrate_volume(volume, host, new_type): LOG.warning(_LW("Storage-assisted migration failed during " "retype.")) return False else: # Modify lun to change policy self.modify_lun(lun_id, change_opts) - return True except exception.VolumeBackendAPIException: LOG.exception(_LE('Retype volume error.')) return False + if replica_enabled_change and replica_enabled_change[1] == 'true': + try: + # If replica_enabled_change is not None, the + # replica_type_change won't be None. See function + # determine_changes_when_retype. + lun_info = self.client.get_lun_info(lun_id) + replica_info = self.replica.create_replica( + lun_info, replica_type_change[1]) + model_update.update(replica_info) + except exception.VolumeBackendAPIException: + LOG.exception(_LE('Retype volume error. ' + 'Create replication failed.')) + return False + + return (True, model_update) + def modify_lun(self, lun_id, change_opts): if change_opts.get('partitionid'): old, new = change_opts['partitionid'] @@ -858,6 +979,19 @@ class HuaweiBaseDriver(driver.VolumeDriver): migration = True change_opts['LUNType'] = (old_opts['LUNType'], new_opts['LUNType']) + volume_type = self._get_volume_type(volume) + volume_opts = self._get_volume_params(volume_type) + if (volume_opts['replication_enabled'] == 'true' + or new_opts['replication_enabled'] == 'true'): + # If replication_enabled changes, + # then replication_type in change_opts will be set. + change_opts['replication_enabled'] = ( + volume_opts['replication_enabled'], + new_opts['replication_enabled']) + + change_opts['replication_type'] = (volume_opts['replication_type'], + new_opts['replication_type']) + change_opts = self._check_needed_changes(lun_id, old_opts, new_opts, change_opts, new_type) @@ -1060,6 +1194,7 @@ class HuaweiBaseDriver(driver.VolumeDriver): # Check other stuffs to determine whether this LUN can be imported. self._check_lun_valid_for_manage(lun_info, external_ref) type_id = volume.get('volume_type_id') + new_opts = None if type_id: # Handle volume type if specified. old_opts = self.get_lun_specs(lun_id) @@ -1087,7 +1222,21 @@ class HuaweiBaseDriver(driver.VolumeDriver): self.client.rename_lun(lun_id, new_name, # pylint: disable=E1121 description) - return {'provider_location': lun_id} + model_update = {} + model_update.update({'provider_location': lun_id}) + + if new_opts and new_opts.get('replication_enabled'): + LOG.debug("Manage volume need to create replication.") + try: + lun_info = self.client.get_lun_info(lun_id) + replica_info = self.replica.create_replica( + lun_info, new_opts.get('replication_type')) + model_update.update(replica_info) + except exception.VolumeBackendAPIException: + with excutils.save_and_reraise_exception(): + LOG.exception(_LE("Manage exist volume failed.")) + + return model_update def _get_lun_info_by_ref(self, external_ref): LOG.debug("Get external_ref: %s", external_ref) @@ -1236,6 +1385,27 @@ class HuaweiBaseDriver(driver.VolumeDriver): {'snapshot_id': snapshot['id'], 'snapshot_name': snapshot_name}) + def replication_enable(self, context, volume): + """Enable replication and do switch role when needed.""" + self.replica.enable_replica(volume) + + def replication_disable(self, context, volume): + """Disable replication.""" + self.replica.disable_replica(volume) + + def replication_failover(self, context, volume, secondary): + """Disable replication and unprotect remote LUN.""" + return self.replica.failover_replica(volume) + + def list_replication_targets(self, context, vref): + """Obtain volume repliction targets.""" + return self.replica.list_replica_targets(vref) + + def get_replication_updates(self, context): + # NOTE(jlc): The manager does not do aynthing with these updates. + # When that is changed, here must be modified as well. + return [] + class HuaweiISCSIDriver(HuaweiBaseDriver, driver.ISCSIDriver): """ISCSI driver for Huawei storage arrays. @@ -1254,9 +1424,10 @@ class HuaweiISCSIDriver(HuaweiBaseDriver, driver.ISCSIDriver): 2.0.1 - Manage/unmanage volume support 2.0.2 - Refactor HuaweiISCSIDriver 2.0.3 - Manage/unmanage snapshot support + 2.0.5 - Replication V2 support """ - VERSION = "2.0.3" + VERSION = "2.0.5" def __init__(self, *args, **kwargs): super(HuaweiISCSIDriver, self).__init__(*args, **kwargs) @@ -1449,9 +1620,10 @@ class HuaweiFCDriver(HuaweiBaseDriver, driver.FibreChannelDriver): 2.0.2 - Refactor HuaweiFCDriver 2.0.3 - Manage/unmanage snapshot support 2.0.4 - Balanced FC port selection + 2.0.5 - Replication V2 support """ - VERSION = "2.0.4" + VERSION = "2.0.5" def __init__(self, *args, **kwargs): super(HuaweiFCDriver, self).__init__(*args, **kwargs) @@ -1515,7 +1687,7 @@ class HuaweiFCDriver(HuaweiBaseDriver, driver.FibreChannelDriver): if not wwns_in_host and not iqns_in_host: self.client.remove_host(host_id) - msg = (_('Can not add FC initiator to host.')) + msg = _('Can not add FC initiator to host.') LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) diff --git a/cinder/volume/drivers/huawei/replication.py b/cinder/volume/drivers/huawei/replication.py new file mode 100644 index 00000000000..a6d57fb8836 --- /dev/null +++ b/cinder/volume/drivers/huawei/replication.py @@ -0,0 +1,675 @@ +# Copyright (c) 2016 Huawei Technologies Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import json +import re + +from oslo_log import log as logging +from oslo_utils import excutils + +from cinder import exception +from cinder.i18n import _, _LW, _LE +from cinder.volume.drivers.huawei import constants +from cinder.volume.drivers.huawei import huawei_utils +from cinder.volume.drivers.huawei import rest_client +from cinder.volume import utils as volume_utils + +LOG = logging.getLogger(__name__) + + +class AbsReplicaOp(object): + def __init__(self, client): + self.client = client + + def create(self, **kwargs): + pass + + def delete(self, replica_id): + pass + + def protect_second(self, replica_id): + pass + + def unprotect_second(self, replica_id): + pass + + def sync(self, replica_id): + pass + + def split(self, replica_id): + pass + + def switch(self, replica_id): + pass + + def is_primary(self, replica_info): + flag = replica_info.get('ISPRIMARY') + if flag and flag.lower() == 'true': + return True + return False + + def get_replica_info(self, replica_id): + return {} + + def _is_status(self, status_key, status, replica_info): + if type(status) in (list, tuple): + return replica_info.get(status_key, '') in status + if type(status) is str: + return replica_info.get(status_key, '') == status + + return False + + def is_running_status(self, status, replica_info): + return self._is_status(constants.REPLICA_RUNNING_STATUS_KEY, + status, replica_info) + + def is_health_status(self, status, replica_info): + return self._is_status(constants.REPLICA_HEALTH_STATUS_KEY, + status, replica_info) + + +class PairOp(AbsReplicaOp): + def create(self, local_lun_id, rmt_lun_id, rmt_dev_id, + rmt_dev_name, replica_model, + speed=constants.REPLICA_SPEED, + period=constants.REPLICA_PERIOD, + **kwargs): + super(PairOp, self).create(**kwargs) + + params = { + "LOCALRESID": local_lun_id, + "LOCALRESTYPE": '11', + "REMOTEDEVICEID": rmt_dev_id, + "REMOTEDEVICENAME": rmt_dev_name, + "REMOTERESID": rmt_lun_id, + "REPLICATIONMODEL": replica_model, + # recovery policy. 1: auto, 2: manual + "RECOVERYPOLICY": '2', + "SPEED": speed, + } + + if replica_model == constants.REPLICA_ASYNC_MODEL: + # Synchronize type values: + # 1, manual + # 2, timed wait when synchronization begins + # 3, timed wait when synchronization ends + params['SYNCHRONIZETYPE'] = '2' + params['TIMINGVAL'] = period + + try: + pair_info = self.client.create_pair(params) + except Exception as err: + msg = _('Create replication pair failed. Error: %s.') % err + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + return pair_info + + def split(self, pair_id): + self.client.split_pair(pair_id) + + def delete(self, pair_id, force=False): + self.client.delete_pair(pair_id, force) + + def protect_second(self, pair_id): + self.client.set_pair_second_access(pair_id, + constants.REPLICA_SECOND_RO) + + def unprotect_second(self, pair_id): + self.client.set_pair_second_access(pair_id, + constants.REPLICA_SECOND_RW) + + def sync(self, pair_id): + self.client.sync_pair(pair_id) + + def switch(self, pair_id): + self.client.switch_pair(pair_id) + + def get_replica_info(self, pair_id): + return self.client.get_pair_by_id(pair_id) + + +class CGOp(AbsReplicaOp): + pass + + +class ReplicaCommonDriver(object): + def __init__(self, conf, replica_op): + self.conf = conf + self.op = replica_op + + def protect_second(self, replica_id): + info = self.op.get_replica_info(replica_id) + if info.get('SECRESACCESS') == constants.REPLICA_SECOND_RO: + return + + self.op.protect_second(replica_id) + self.wait_second_access(replica_id, constants.REPLICA_SECOND_RO) + + def unprotect_second(self, replica_id): + info = self.op.get_replica_info(replica_id) + if info.get('SECRESACCESS') == constants.REPLICA_SECOND_RW: + return + + self.op.unprotect_second(replica_id) + self.wait_second_access(replica_id, constants.REPLICA_SECOND_RW) + + def sync(self, replica_id, wait_complete=False): + self.protect_second(replica_id) + + expect_status = (constants.REPLICA_RUNNING_STATUS_NORMAL, + constants.REPLICA_RUNNING_STATUS_SYNC, + constants.REPLICA_RUNNING_STATUS_INITIAL_SYNC) + info = self.op.get_replica_info(replica_id) + + # When running status is synchronizing or normal, + # it's not necessary to do synchronize again. + if (info.get('REPLICATIONMODEL') == constants.REPLICA_SYNC_MODEL + and self.op.is_running_status(expect_status, info)): + return + + self.op.sync(replica_id) + self.wait_expect_state(replica_id, expect_status) + + if wait_complete: + self.wait_replica_ready(replica_id) + + def split(self, replica_id): + running_status = (constants.REPLICA_RUNNING_STATUS_SPLIT, + constants.REPLICA_RUNNING_STATUS_INVALID) + info = self.op.get_replica_info(replica_id) + if self.op.is_running_status(running_status, info): + return + + try: + self.op.split(replica_id) + except Exception as err: + LOG.warning(_LW('Split replication exception: %s.'), err) + + try: + self.wait_expect_state(replica_id, running_status) + except Exception as err: + msg = _('Split replication failed.') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def enable(self, replica_id, wait_sync_complete=False): + info = self.op.get_replica_info(replica_id) + if not self.op.is_primary(info): + self.switch(replica_id) + self.sync(replica_id) + return None + + def disable(self, replica_id): + self.split(replica_id) + return None + + def switch(self, replica_id): + self.split(replica_id) + self.unprotect_second(replica_id) + self.op.switch(replica_id) + + # Wait to be primary + def _wait_switch_to_primary(): + info = self.op.get_replica_info(replica_id) + if self.op.is_primary(info): + return True + return False + + interval = constants.DEFAULT_REPLICA_WAIT_INTERVAL + timeout = constants.DEFAULT_REPLICA_WAIT_TIMEOUT + huawei_utils.wait_for_condition(_wait_switch_to_primary, + interval, + timeout) + + def failover(self, replica_id): + """Failover replication. + + Purpose: + 1. Split replication. + 2. Set secondary access read & write. + """ + info = self.op.get_replica_info(replica_id) + if self.op.is_primary(info): + msg = _('We should not do switch over on primary array.') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + sync_status_set = (constants.REPLICA_RUNNING_STATUS_SYNC, + constants.REPLICA_RUNNING_STATUS_INITIAL_SYNC) + if self.op.is_running_status(sync_status_set, info): + self.wait_replica_ready(replica_id) + + self.split(replica_id) + self.op.unprotect_second(replica_id) + + def wait_replica_ready(self, replica_id, interval=None, timeout=None): + LOG.debug('Wait synchronize complete.') + running_status_normal = (constants.REPLICA_RUNNING_STATUS_NORMAL, + constants.REPLICA_RUNNING_STATUS_SYNCED) + running_status_sync = (constants.REPLICA_RUNNING_STATUS_SYNC, + constants.REPLICA_RUNNING_STATUS_INITIAL_SYNC) + health_status_normal = constants.REPLICA_HEALTH_STATUS_NORMAL + + def _replica_ready(): + info = self.op.get_replica_info(replica_id) + if (self.op.is_running_status(running_status_normal, info) + and self.op.is_health_status(health_status_normal, info)): + return True + + if not self.op.is_running_status(running_status_sync, info): + msg = (_('Wait synchronize failed. Running status: %s.') % + info.get(constants.REPLICA_RUNNING_STATUS_KEY)) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + return False + + if not interval: + interval = constants.DEFAULT_WAIT_INTERVAL + if not timeout: + timeout = constants.DEFAULT_WAIT_TIMEOUT + + huawei_utils.wait_for_condition(_replica_ready, + interval, + timeout) + + def wait_second_access(self, replica_id, access_level): + def _check_access(): + info = self.op.get_replica_info(replica_id) + if info.get('SECRESACCESS') == access_level: + return True + return False + + interval = constants.DEFAULT_REPLICA_WAIT_INTERVAL + timeout = constants.DEFAULT_REPLICA_WAIT_TIMEOUT + huawei_utils.wait_for_condition(_check_access, + interval, + timeout) + + def wait_expect_state(self, replica_id, + running_status, health_status=None, + interval=None, timeout=None): + def _check_state(): + info = self.op.get_replica_info(replica_id) + if self.op.is_running_status(running_status, info): + if (not health_status + or self.op.is_health_status(health_status, info)): + return True + return False + + if not interval: + interval = constants.DEFAULT_REPLICA_WAIT_INTERVAL + if not timeout: + timeout = constants.DEFAULT_REPLICA_WAIT_TIMEOUT + + huawei_utils.wait_for_condition(_check_state, interval, timeout) + + +def get_replication_driver_data(volume): + if volume.get('replication_driver_data'): + return json.loads(volume['replication_driver_data']) + return {} + + +def to_string(dict_data): + if dict_data: + return json.dumps(dict_data) + return '' + + +class ReplicaPairManager(object): + def __init__(self, local_client, conf): + self.local_client = local_client + self.conf = conf + self.replica_device = self.conf.safe_get('replication_device') + if not self.replica_device: + return + + # managed_backed_name format: host_name@backend_name#pool_name + self.rmt_backend = self.replica_device[0]['managed_backend_name'] + self.rmt_pool = volume_utils.extract_host(self.rmt_backend, + level='pool') + self.target_dev_id = self.replica_device[0]['target_device_id'] + + self._init_rmt_client() + self.local_op = PairOp(self.local_client) + self.local_driver = ReplicaCommonDriver(self.conf, self.local_op) + self.rmt_op = PairOp(self.rmt_client) + self.rmt_driver = ReplicaCommonDriver(self.conf, self.rmt_op) + + self.try_login_remote_array() + + def try_login_remote_array(self): + try: + self.rmt_client.login() + except Exception as err: + LOG.warning(_LW('Remote array login failed. Error: %s.'), err) + + def try_get_remote_wwn(self): + try: + info = self.rmt_client.get_array_info() + return info.get('wwn') + except Exception as err: + LOG.warning(_LW('Get remote array wwn failed. Error: %s.'), err) + return None + + def get_remote_device_by_wwn(self, wwn): + devices = {} + try: + devices = self.local_client.get_remote_devices() + except Exception as err: + LOG.warning(_LW('Get remote devices failed. Error: %s.'), err) + + for device in devices: + if device.get('WWN') == wwn: + return device + + return {} + + def check_remote_available(self): + if not self.replica_device: + return False + + # We get device wwn in every check time. + # If remote array changed, we can run normally. + wwn = self.try_get_remote_wwn() + if not wwn: + return False + + device = self.get_remote_device_by_wwn(wwn) + # Check remote device is available to use. + # If array type is replication, 'ARRAYTYPE' == '1'. + # If health status is normal, 'HEALTHSTATUS' == '1'. + if (device and device.get('ARRAYTYPE') == '1' + and device.get('HEALTHSTATUS') == '1' + and device.get('RUNNINGSTATUS') == constants.STATUS_RUNNING): + return True + + return False + + def update_replica_capability(self, stats): + is_rmt_dev_available = self.check_remote_available() + if not is_rmt_dev_available: + if self.replica_device: + LOG.warning(_LW('Remote device is unavailable. ' + 'Remote backend: %s.'), + self.rmt_backend) + return stats + + for pool in stats['pools']: + pool['replication_enabled'] = True + pool['replication_type'] = ['sync', 'async'] + + return stats + + def _init_rmt_client(self): + # Multiple addresses support. + rmt_addrs = self.replica_device[0]['san_address'].split(';') + rmt_addrs = list(set([x.strip() for x in rmt_addrs if x.strip()])) + rmt_user = self.replica_device[0]['san_user'] + rmt_password = self.replica_device[0]['san_password'] + self.rmt_client = rest_client.RestClient(self.conf, + rmt_addrs, + rmt_user, + rmt_password) + + def get_rmt_dev_info(self): + wwn = self.try_get_remote_wwn() + if not wwn: + return None, None + + device = self.get_remote_device_by_wwn(wwn) + if not device: + return None, None + + return device.get('ID'), device.get('NAME') + + def build_rmt_lun_params(self, local_lun_info): + params = { + 'TYPE': '11', + 'NAME': local_lun_info['NAME'], + 'PARENTTYPE': '216', + 'PARENTID': self.rmt_client.get_pool_id(self.rmt_pool), + 'DESCRIPTION': local_lun_info['DESCRIPTION'], + 'ALLOCTYPE': local_lun_info['ALLOCTYPE'], + 'CAPACITY': local_lun_info['CAPACITY'], + 'WRITEPOLICY': self.conf.lun_write_type, + 'MIRRORPOLICY': self.conf.lun_mirror_switch, + 'PREFETCHPOLICY': self.conf.lun_prefetch_type, + 'PREFETCHVALUE': self.conf.lun_prefetch_value, + 'DATATRANSFERPOLICY': self.conf.lun_policy, + 'READCACHEPOLICY': self.conf.lun_read_cache_policy, + 'WRITECACHEPOLICY': self.conf.lun_write_cache_policy, + } + + LOG.debug('Remote lun params: %s.', params) + return params + + def wait_volume_online(self, client, lun_info, + interval=None, timeout=None): + online_status = constants.STATUS_VOLUME_READY + if lun_info.get('RUNNINGSTATUS') == online_status: + return + + lun_id = lun_info['ID'] + + def _wait_online(): + info = client.get_lun_info(lun_id) + return info.get('RUNNINGSTATUS') == online_status + + if not interval: + interval = constants.DEFAULT_REPLICA_WAIT_INTERVAL + if not timeout: + timeout = constants.DEFAULT_REPLICA_WAIT_TIMEOUT + + huawei_utils.wait_for_condition(_wait_online, + interval, + timeout) + + def create_rmt_lun(self, local_lun_info): + # Create on rmt array. If failed, raise exception. + lun_params = self.build_rmt_lun_params(local_lun_info) + lun_info = self.rmt_client.create_lun(lun_params) + try: + self.wait_volume_online(self.rmt_client, lun_info) + except exception.VolumeBackendAPIException: + with excutils.save_and_reraise_exception(): + self.rmt_client.delete_lun(lun_info['ID']) + + return lun_info + + def create_replica(self, local_lun_info, replica_model): + """Create remote LUN and replication pair. + + Purpose: + 1. create remote lun + 2. create replication pair + 3. enable replication pair + """ + LOG.debug(('Create replication, local lun info: %(info)s, ' + 'replication model: %(model)s.'), + {'info': local_lun_info, 'model': replica_model}) + + local_lun_id = local_lun_info['ID'] + self.wait_volume_online(self.local_client, local_lun_info) + + # step1, create remote lun + rmt_lun_info = self.create_rmt_lun(local_lun_info) + rmt_lun_id = rmt_lun_info['ID'] + + # step2, get remote device info + rmt_dev_id, rmt_dev_name = self.get_rmt_dev_info() + if not rmt_lun_id or not rmt_dev_name: + self._delete_rmt_lun(rmt_lun_id) + msg = _('Get remote device info failed.') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + # step3, create replication pair + try: + pair_info = self.local_op.create(local_lun_id, + rmt_lun_id, rmt_dev_id, + rmt_dev_name, replica_model) + pair_id = pair_info['ID'] + except Exception as err: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Create pair failed. Error: %s.'), err) + self._delete_rmt_lun(rmt_lun_id) + + # step4, start sync manually. If replication type is sync, + # then wait for sync complete. + wait_complete = (replica_model == constants.REPLICA_SYNC_MODEL) + try: + self.local_driver.sync(pair_id, wait_complete) + except Exception as err: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Start synchronization failed. Error: %s.'), err) + self._delete_pair(pair_id) + self._delete_rmt_lun(rmt_lun_id) + + model_update = {} + driver_data = {'pair_id': pair_id, + 'rmt_lun_id': rmt_lun_id} + model_update['replication_driver_data'] = to_string(driver_data) + model_update['replication_status'] = 'enabled' + LOG.debug('Create replication, return info: %s.', model_update) + return model_update + + def _delete_pair(self, pair_id): + if (not pair_id + or not self.local_client.check_pair_exist(pair_id)): + return + + self.local_driver.split(pair_id) + self.local_op.delete(pair_id) + + def _delete_rmt_lun(self, lun_id): + if lun_id and self.rmt_client.check_lun_exist(lun_id): + self.rmt_client.delete_lun(lun_id) + + def delete_replica(self, volume): + """Delete replication pair and remote lun. + + Purpose: + 1. delete replication pair + 2. delete remote_lun + """ + LOG.debug('Delete replication, volume: %s.', volume['id']) + info = get_replication_driver_data(volume) + pair_id = info.get('pair_id') + if pair_id: + self._delete_pair(pair_id) + + # Delete remote_lun + rmt_lun_id = info.get('rmt_lun_id') + if rmt_lun_id: + self._delete_rmt_lun(rmt_lun_id) + + def enable_replica(self, volume): + """Enable replication. + + Purpose: + 1. If local backend's array is secondary, switch to primary + 2. Synchronize data + """ + LOG.debug('Enable replication, volume: %s.', volume['id']) + + info = get_replication_driver_data(volume) + pair_id = info.get('pair_id') + if not pair_id: + msg = _('No pair id in volume replication_driver_data.') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + info = self.local_op.get_replica_info(pair_id) + if not info: + msg = _('Pair does not exist on array. Pair id: %s.') % pair_id + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + wait_sync_complete = False + if info.get('REPLICATIONMODEL') == constants.REPLICA_SYNC_MODEL: + wait_sync_complete = True + + return self.local_driver.enable(pair_id, wait_sync_complete) + + def disable_replica(self, volume): + """We consider that all abnormal states is disabled.""" + LOG.debug('Disable replication, volume: %s.', volume['id']) + + info = get_replication_driver_data(volume) + pair_id = info.get('pair_id') + if not pair_id: + LOG.warning(_LW('No pair id in volume replication_driver_data.')) + return None + + return self.local_driver.disable(pair_id) + + def failover_replica(self, volume): + """Just make the secondary available.""" + LOG.debug('Failover replication, volume: %s.', volume['id']) + + info = get_replication_driver_data(volume) + pair_id = info.get('pair_id') + if not pair_id: + msg = _('No pair id in volume replication_driver_data.') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + rmt_lun_id = info.get('rmt_lun_id') + if not rmt_lun_id: + msg = _('No remote LUN id in volume replication_driver_data.') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + # Remote array must be available. So we can get the real pool info. + lun_info = self.rmt_client.get_lun_info(rmt_lun_id) + lun_wwn = lun_info.get('WWN') + lun_pool = lun_info.get('PARENTNAME') + new_backend = re.sub(r'(?<=#).*$', lun_pool, self.rmt_backend) + + self.rmt_driver.failover(pair_id) + + metadata = huawei_utils.get_volume_metadata(volume) + metadata.update({'lun_wwn': lun_wwn}) + + new_driver_data = {'pair_id': pair_id, + 'rmt_lun_id': volume['provider_location']} + new_driver_data = to_string(new_driver_data) + return {'host': new_backend, + 'provider_location': rmt_lun_id, + 'replication_driver_data': new_driver_data, + 'metadata': metadata} + + def list_replica_targets(self, volume): + info = get_replication_driver_data(volume) + if not info: + LOG.warning(_LW('Replication driver data does not exist. ' + 'Volume: %s'), volume['id']) + + targets = [{'target_device_id': self.target_dev_id}] + return {'volume_id': volume['id'], + 'targets': targets} + + +def get_replication_opts(opts): + if opts.get('replication_type') == 'sync': + opts['replication_type'] = constants.REPLICA_SYNC_MODEL + else: + opts['replication_type'] = constants.REPLICA_ASYNC_MODEL + + return opts diff --git a/cinder/volume/drivers/huawei/rest_client.py b/cinder/volume/drivers/huawei/rest_client.py index e926cf59f0f..64d3189e280 100644 --- a/cinder/volume/drivers/huawei/rest_client.py +++ b/cinder/volume/drivers/huawei/rest_client.py @@ -231,7 +231,7 @@ class RestClient(object): return info - def get_pool_id(self, volume, pool_name): + def get_pool_id(self, pool_name): pools = self.get_all_pools() pool_info = self.get_pool_info(pool_name, pools) if not pool_info: @@ -1562,11 +1562,15 @@ class RestClient(object): self._assert_rest_result(result, _('Add lun to cache error.')) - def find_array_version(self): + def get_array_info(self): url = "/system/" result = self.call(url, None, "GET") - self._assert_rest_result(result, _('Find array version error.')) - return result['data']['PRODUCTVERSION'] + self._assert_rest_result(result, _('Get array info error.')) + return result.get('data', None) + + def find_array_version(self): + info = self.get_array_info() + return info.get('PRODUCTVERSION', None) def remove_host(self, host_id): url = "/host/%s" % host_id @@ -2008,3 +2012,78 @@ class RestClient(object): for item in result.get('data', []): wwns.append(item['WWN']) return wwns + + def get_remote_devices(self): + url = "/remote_device" + result = self.call(url, None, "GET") + self._assert_rest_result(result, _('Get remote devices error.')) + return result.get('data', []) + + def create_pair(self, pair_params): + url = "/REPLICATIONPAIR" + result = self.call(url, pair_params, "POST") + + msg = _('Create replication error.') + self._assert_rest_result(result, msg) + self._assert_data_in_result(result, msg) + return result['data'] + + def get_pair_by_id(self, pair_id): + url = "/REPLICATIONPAIR/" + pair_id + result = self.call(url, None, "GET") + + msg = _('Get pair failed.') + self._assert_rest_result(result, msg) + return result.get('data', {}) + + def switch_pair(self, pair_id): + url = '/REPLICATIONPAIR/switch' + data = {"ID": pair_id, + "TYPE": "263"} + result = self.call(url, data, "PUT") + + msg = _('Switch over pair error.') + self._assert_rest_result(result, msg) + + def split_pair(self, pair_id): + url = '/REPLICATIONPAIR/split' + data = {"ID": pair_id, + "TYPE": "263"} + result = self.call(url, data, "PUT") + + msg = _('Split pair error.') + self._assert_rest_result(result, msg) + + def delete_pair(self, pair_id, force=False): + url = "/REPLICATIONPAIR/" + pair_id + data = None + if force: + data = {"ISLOCALDELETE": force} + + result = self.call(url, data, "DELETE") + + msg = _('delete_replication error.') + self._assert_rest_result(result, msg) + + def sync_pair(self, pair_id): + url = "/REPLICATIONPAIR/sync" + data = {"ID": pair_id, + "TYPE": "263"} + result = self.call(url, data, "PUT") + + msg = _('Sync pair error.') + self._assert_rest_result(result, msg) + + def check_pair_exist(self, pair_id): + url = "/REPLICATIONPAIR/" + pair_id + result = self.call(url, None, "GET") + return result['error']['code'] == 0 + + def set_pair_second_access(self, pair_id, access): + url = "/REPLICATIONPAIR/" + pair_id + data = {"ID": pair_id, + "SECRESACCESS": access} + result = self.call(url, data, "PUT") + + msg = _('Set pair secondary access error.') + self._assert_rest_result(result, msg) diff --git a/releasenotes/notes/support-replication-for-huawei-volume-driver-ad61e9f5eba9c422.yaml b/releasenotes/notes/support-replication-for-huawei-volume-driver-ad61e9f5eba9c422.yaml new file mode 100644 index 00000000000..2ff013f545e --- /dev/null +++ b/releasenotes/notes/support-replication-for-huawei-volume-driver-ad61e9f5eba9c422.yaml @@ -0,0 +1,2 @@ +features: + - Added Replication V2 support for Huawei drivers.