diff --git a/cinder/tests/unit/volume/drivers/test_fujitsu_dx.py b/cinder/tests/unit/volume/drivers/test_fujitsu_dx.py index 31829e8bd79..58d33deac1e 100644 --- a/cinder/tests/unit/volume/drivers/test_fujitsu_dx.py +++ b/cinder/tests/unit/volume/drivers/test_fujitsu_dx.py @@ -91,6 +91,16 @@ TEST_CLONE = { 'host': 'controller@113#abcd1234_TPP' } +TEST_VOLUME_QOS = { + 'id': '7bd8b81f-137d-4140-85ce-d00281c91c84', + 'name': 'qos', + 'display_name': 'qos', + 'provider_location': None, + 'metadata': {}, + 'size': 1, + 'host': 'controller@113#abcd1234_TPP' +} + ISCSI_INITIATOR = 'iqn.1993-08.org.debian:01:8261afe17e4c' ISCSI_TARGET_IP = '10.0.0.3' ISCSI_TARGET_IQN = 'iqn.2000-09.com.fujitsu:storage-system.eternus-dxl:0' @@ -98,6 +108,9 @@ FC_TARGET_WWN = ['500000E0DA000001', '500000E0DA000002'] TEST_WWPN = ['0123456789111111', '0123456789222222'] TEST_CONNECTOR = {'initiator': ISCSI_INITIATOR, 'wwpns': TEST_WWPN} +STORAGE_IP = '172.16.0.2' +TEST_USER = 'testuser' +TEST_PASSWORD = 'testpassword' STOR_CONF_SVC = 'FUJITSU_StorageConfigurationService' CTRL_CONF_SVC = 'FUJITSU_ControllerConfigurationService' @@ -128,6 +141,9 @@ FAKE_LUN_NO2 = '0x001E' # Volume2 in pool abcd1234_RG FAKE_LUN_ID3 = '600000E00D2800000028075301140000' FAKE_LUN_NO3 = '0x0114' +# VolumeQoS in pool abcd1234_TPP +FAKE_LUN_ID_QOS = '600000E00D2A0000002A011500140000' +FAKE_LUN_NO_QOS = '0x0014' FAKE_SYSTEM_NAME = 'ET603SA4621302115' # abcd1234_TPP pool FAKE_USEGB = 2.0 @@ -160,20 +176,20 @@ FAKE_POOLS = [{ }] FAKE_STATS = { - 'driver_version': '1.3.0', + 'driver_version': '1.4.0', 'storage_protocol': 'iSCSI', 'vendor_name': 'FUJITSU', - 'QoS_support': False, + 'QoS_support': True, 'volume_backend_name': 'volume_backend_name', 'shared_targets': True, 'backend_state': 'up', 'pools': FAKE_POOLS, } FAKE_STATS2 = { - 'driver_version': '1.3.0', + 'driver_version': '1.4.0', 'storage_protocol': 'FC', 'vendor_name': 'FUJITSU', - 'QoS_support': False, + 'QoS_support': True, 'volume_backend_name': 'volume_backend_name', 'shared_targets': True, 'backend_state': 'up', @@ -183,37 +199,58 @@ FAKE_STATS2 = { # Volume1 in pool abcd1234_TPP FAKE_KEYBIND1 = { - 'CreationClassName': 'FUJITSU_StorageVolume', 'SystemName': STORAGE_SYSTEM, 'DeviceID': FAKE_LUN_ID1, - 'SystemCreationClassName': 'FUJITSU_StorageComputerSystem', } # Volume2 in pool abcd1234_RG FAKE_KEYBIND3 = { - 'CreationClassName': 'FUJITSU_StorageVolume', 'SystemName': STORAGE_SYSTEM, 'DeviceID': FAKE_LUN_ID3, - 'SystemCreationClassName': 'FUJITSU_StorageComputerSystem', +} + +# Volume QOS in pool abcd1234_TPP +FAKE_KEYBIND_QOS = { + 'SystemName': STORAGE_SYSTEM, + 'DeviceID': FAKE_LUN_ID_QOS, } # Volume1 FAKE_LOCATION1 = { 'classname': 'FUJITSU_StorageVolume', 'keybindings': FAKE_KEYBIND1, + 'vol_name': 'FJosv_0qJ4rpOHgFE8ipcJOMfBmg==' +} + +# Clone Volume +FAKE_CLONE_LOCATION = { + 'classname': 'FUJITSU_StorageVolume', + 'keybindings': FAKE_KEYBIND1, + 'vol_name': 'FJosv_UkCZqMFZW3SU_JzxjHiKfg==' } # Volume2 FAKE_LOCATION3 = { 'classname': 'FUJITSU_StorageVolume', 'keybindings': FAKE_KEYBIND3, + 'vol_name': 'FJosv_4whcadwDac7ANKHA2O719A==' +} + +# VolumeQOS +FAKE_LOCATION_QOS = { + 'classname': 'FUJITSU_StorageVolume', + 'keybindings': FAKE_KEYBIND_QOS, + 'vol_name': 'FJosv_mIsapeuZOaSXz4LYTqFcug==' } # Volume1 metadata info. +# Here is a misspelling, and the right value should be "Thinprovisioning_POOL". +# It would not be compatible with the metadata of the legacy volumes, +# so this spelling mistake needs to be retained. FAKE_LUN_META1 = { 'FJ_Pool_Type': 'Thinporvisioning_POOL', 'FJ_Volume_No': FAKE_LUN_NO1, - 'FJ_Volume_Name': u'FJosv_0qJ4rpOHgFE8ipcJOMfBmg==', + 'FJ_Volume_Name': 'FJosv_0qJ4rpOHgFE8ipcJOMfBmg==', 'FJ_Pool_Name': STORAGE_TYPE, 'FJ_Backend': FAKE_SYSTEM_NAME, } @@ -222,10 +259,20 @@ FAKE_LUN_META1 = { FAKE_LUN_META3 = { 'FJ_Pool_Type': 'RAID_GROUP', 'FJ_Volume_No': FAKE_LUN_NO3, - 'FJ_Volume_Name': u'FJosv_4whcadwDac7ANKHA2O719A==', + 'FJ_Volume_Name': 'FJosv_4whcadwDac7ANKHA2O719A==', 'FJ_Pool_Name': STORAGE_TYPE2, 'FJ_Backend': FAKE_SYSTEM_NAME, } + +# VolumeQOS metadata info +FAKE_LUN_META_QOS = { + 'FJ_Pool_Type': 'Thinporvisioning_POOL', + 'FJ_Volume_No': FAKE_LUN_NO_QOS, + 'FJ_Volume_Name': 'FJosv_mIsapeuZOaSXz4LYTqFcug==', + 'FJ_Pool_Name': STORAGE_TYPE, + 'FJ_Backend': FAKE_SYSTEM_NAME, +} + # Volume1 FAKE_MODEL_INFO1 = { 'provider_location': six.text_type(FAKE_LOCATION1), @@ -236,17 +283,21 @@ FAKE_MODEL_INFO3 = { 'provider_location': six.text_type(FAKE_LOCATION3), 'metadata': FAKE_LUN_META3, } +# VoluemQOS +FAKE_MODEL_INFO_QOS = { + 'provider_location': six.text_type(FAKE_LOCATION_QOS), + 'metadata': FAKE_LUN_META_QOS, +} FAKE_KEYBIND2 = { - 'CreationClassName': 'FUJITSU_StorageVolume', 'SystemName': STORAGE_SYSTEM, 'DeviceID': FAKE_LUN_ID2, - 'SystemCreationClassName': 'FUJITSU_StorageComputerSystem', } FAKE_LOCATION2 = { 'classname': 'FUJITSU_StorageVolume', 'keybindings': FAKE_KEYBIND2, + 'vol_name': 'FJosv_OgEZj1mSvKRvIKOExKktlg==' } FAKE_SNAP_INFO = { @@ -256,16 +307,36 @@ FAKE_SNAP_INFO = { FAKE_LUN_META2 = { 'FJ_Pool_Type': 'Thinporvisioning_POOL', 'FJ_Volume_No': FAKE_LUN_NO1, - 'FJ_Volume_Name': u'FJosv_UkCZqMFZW3SU_JzxjHiKfg==', + 'FJ_Volume_Name': 'FJosv_OgEZj1mSvKRvIKOExKktlg==', + 'FJ_Pool_Name': STORAGE_TYPE, + 'FJ_Backend': FAKE_SYSTEM_NAME, +} + +FAKE_CLONE_LUN_META = { + 'FJ_Pool_Type': 'Thinporvisioning_POOL', + 'FJ_Volume_No': FAKE_LUN_NO1, + 'FJ_Volume_Name': 'FJosv_UkCZqMFZW3SU_JzxjHiKfg==', 'FJ_Pool_Name': STORAGE_TYPE, 'FJ_Backend': FAKE_SYSTEM_NAME, } FAKE_MODEL_INFO2 = { - 'provider_location': six.text_type(FAKE_LOCATION1), - 'metadata': FAKE_LUN_META2, + 'provider_location': six.text_type(FAKE_CLONE_LOCATION), + 'metadata': FAKE_CLONE_LUN_META, } +FAKE_CLI_OUTPUT = { + "result": 0, + 'rc': '0', + "message": 'TEST_MESSAGE' +} + +# Constants for QOS +MAX_IOPS = 4294967295 +MAX_THROUGHPUT = 2097151 +MIN_IOPS = 1 +MIN_THROUGHPUT = 1 + class FJ_StorageVolume(dict): pass @@ -289,6 +360,32 @@ class FakeCIMInstanceName(dict): instancename.namespace = 'root/eternus' return instancename + def fake_enumerateinstances(self): + instancename_1 = FakeCIMInstanceName() + + ret = [] + instancename_1['ElementName'] = 'FJosv_0qJ4rpOHgFE8ipcJOMfBmg==' + instancename_1['Purpose'] = '00228+0x06' + instancename_1['Name'] = None + instancename_1['DeviceID'] = FAKE_LUN_ID1 + instancename_1['SystemName'] = STORAGE_SYSTEM + ret.append(instancename_1) + instancename_1.path = '' + instancename_1.classname = 'FUJITSU_StorageVolume' + + snaps = FakeCIMInstanceName() + snaps['ElementName'] = 'FJosv_OgEZj1mSvKRvIKOExKktlg==' + snaps['Name'] = None + ret.append(snaps) + snaps.path = '' + + map = FakeCIMInstanceName() + map['ElementName'] = 'FJosv_hhJsV9lcMBvAPADrGqucwg==' + map['Name'] = None + ret.append(map) + map.path = '' + return ret + class FakeEternusConnection(object): def InvokeMethod(self, MethodName, Service, ElementName=None, InPool=None, @@ -383,6 +480,9 @@ class FakeEternusConnection(object): result = self._enum_scsiport_endpoint() elif name == 'FUJITSU_StorageHardwareID': result = None + elif name == 'FUJITSU_StorageVolume': + instancename_1 = FakeCIMInstanceName() + result = instancename_1.fake_enumerateinstances() else: result = None @@ -892,6 +992,10 @@ class FJFCDriverTestCase(test.TestCase): instancename.fake_create_eternus_instance_name) self.mock_object(ssh_utils, 'SSHPool', mock.Mock()) + + self.mock_object(dx_common.FJDXCommon, '_get_qos_specs', + return_value={}) + self.mock_object(eternus_dx_cli.FJDXCLI, '_exec_cli_with_eternus', self.fake_exec_cli_with_eternus) # Set fc driver to self.driver. @@ -900,11 +1004,12 @@ class FJFCDriverTestCase(test.TestCase): def fake_exec_cli_with_eternus(self, exec_cmdline): if exec_cmdline == "show users": - ret = ('\r\nCLI> show users\r\n00\r\n' + ret = ('\r\nCLI> %s\r\n00\r\n' '3B\r\nf.ce\tMaintainer\t01\t00' '\t00\t00\r\ntestuser\tSoftware' - '\t01\t01\t00\t00\r\nCLI> ') - return ret + '\t01\t01\t00\t00\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('set volume-qos'): + ret = '%s\r\n00\r\n0001\r\nCLI> ' % exec_cmdline elif exec_cmdline.startswith('show volumes'): ret = ('\r\nCLI> %s\r\n00\r\n0560\r\n0000' '\tFJosv_0qJ4rpOHgFE8ipcJOMfBmg==' @@ -914,14 +1019,69 @@ class FJFCDriverTestCase(test.TestCase): '\tFF\t20\tFF\tFFFF\t00' '\t600000E00D2A0000002A011500140000' '\t00\t00\tFF\tFF\tFFFFFFFF\t00' - '\t00\tFF\r\n0001\tFJosv_UkCZqMFZW3SU_JzxjHiKfg==' + '\t00\tFF\r\n0001\tFJosv_OgEZj1mSvKRvIKOExKktlg==' '\tA001\t0B\t00\t0000\tabcd1234_OSVD' '\t0000000000200000\t00\t00\t00000000' '\t0050\tFF\t00\tFF\tFF\t20\tFF\tFFFF' '\t00\t600000E00D2A0000002A0115001E0000' '\t00\t00\tFF\tFF\tFFFFFFFF\t00' '\t00\tFF' % exec_cmdline) - return ret + elif exec_cmdline.startswith('show enclosure-status'): + ret = ('\r\nCLI> %s\r\n00\r\n' + 'ETDX200S3_1\t01\tET203ACU\t4601417434\t280753\t20' + '\t00\t00\t01\t02\t01001000\tV10L87-9000\t91\r\n02' + '\r\n70000000\t30\r\nD0000100\t30\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('show volume-qos'): + ret = ('\r\nCLI> %s\r\n00\r\n' + '0002\t\r\n0000\tFJosv_0qJ4rpOHgFE8ipcJOMfBmg==\t0F' + '\t\r\n0001\tFJosv_OgEZj1mSvKRvIKOExKktlg==\t0D' + '\t\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('show qos-bandwidth-limit'): + ret = ('\r\nCLI> %s\r\n00\r\n0010\t\r\n00\t0000ffff\t0000ffff' + '\t0000ffff\t0000ffff\t0000ffff\t0000ffff\t0000ffff' + '\t0000ffff\t0000ffff\t0000ffff\t0000ffff\t0000ffff\r\n' + '01\t00000001\t00000001\t00000001\t00000001\t00000001' + '\t00000001\t00000001\t00000001\t00000001\t00000001' + '\t00000001\t00000001\r\n02\t00000002\t00000002\t00000002' + '\t00000002\t00000002\t00000002\t00000002\t00000002' + '\t00000002\t00000002\t00000002\t00000002\r\n03\t00000003' + '\t00000003\t00000003\t00000003\t00000003\t00000003' + '\t00000003\t00000003\t00000003\t00000003\t00000003' + '\t00000003\r\n04\t00000004\t00000004\t00000004\t00000004' + '\t00000004\t00000004\t00000004\t00000004\t00000004' + '\t00000004\t00000004\t00000004\r\n05\t00000005\t00000005' + '\t00000005\t00000005\t00000005\t00000005\t00000005' + '\t00000005\t00000005\t00000005\t00000005\t00000005\r\n06' + '\t00000006\t00000006\t00000006\t00000006\t00000006' + '\t00000006\t00000006\t00000006\t00000006\t00000006' + '\t00000006\t00000006\r\n07\t00000007\t00000007\t00000007' + '\t00000007\t00000007\t00000007\t00000007\t00000007' + '\t00000007\t00000007\t00000007\t00000007\r\n08\t00000008' + '\t00000008\t00000008\t00000008\t00000008\t00000008' + '\t00000008\t00000008\t00000008\t00000008\t00000008' + '\t00000008\r\n09\t00000009\t00000009\t00000009\t00000009' + '\t00000009\t00000009\t00000009\t00000009\t00000009' + '\t00000009\t00000009\t00000009\r\n0a\t0000000a\t0000000a' + '\t0000000a\t0000000a\t0000000a\t0000000a\t0000000a' + '\t0000000a\t0000000a\t0000000a\t0000000a\t0000000a\r\n0b' + '\t0000000b\t0000000b\t0000000b\t0000000b\t0000000b' + '\t0000000b\t0000000b\t0000000b\t0000000b\t0000000b' + '\t0000000b\t0000000b\r\n0c\t0000000c\t0000000c\t0000000c' + '\t0000000c\t0000000c\t0000000c\t0000000c\t0000000c' + '\t0000000c\t0000000c\t0000000c\t0000000c\r\n0d\t0000000d' + '\t0000000d\t0000000d\t0000000d\t0000000d\t0000000d' + '\t0000000d\t0000000d\t0000000d\t0000000d\t0000000d' + '\t0000000d\r\n0e\t0000000e\t0000000e\t0000000e\t0000000e' + '\t0000000e\t0000000e\t0000000e\t0000000e\t0000000e' + '\t0000000e\t0000000e\t0000000e\r\n0f\t0000000f\t0000000f' + '\t0000000f\t0000000f\t0000000f\t0000000f\t0000000f' + '\t0000000f\t0000000f\t0000000f\t0000000f\t0000000f' + '\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('set qos-bandwidth-limit'): + ret = '%s\r\n00\r\n0001\r\nCLI> ' % exec_cmdline + else: + ret = None + return ret def fake_safe_get(self, str=None): return str @@ -1030,6 +1190,14 @@ class FJFCDriverTestCase(test.TestCase): self.driver.extend_volume(volume_info, 10) + def test_create_volume_with_qos(self): + self.driver.common._get_qos_specs = mock.Mock() + self.driver.common._get_qos_specs.return_value = {'maxBWS': '700'} + self.driver.common._set_qos = mock.Mock() + model_info = self.driver.create_volume(TEST_VOLUME_QOS) + self.assertEqual(FAKE_MODEL_INFO_QOS, model_info) + self.driver.common._set_qos.assert_called() + class FJISCSIDriverTestCase(test.TestCase): def __init__(self, *args, **kwargs): @@ -1061,6 +1229,10 @@ class FJISCSIDriverTestCase(test.TestCase): self.fake_get_mapdata) self.mock_object(ssh_utils, 'SSHPool', mock.Mock()) + + self.mock_object(dx_common.FJDXCommon, '_get_qos_specs', + return_value={}) + self.mock_object(eternus_dx_cli.FJDXCLI, '_exec_cli_with_eternus', self.fake_exec_cli_with_eternus) # Set iscsi driver to self.driver. @@ -1069,11 +1241,12 @@ class FJISCSIDriverTestCase(test.TestCase): def fake_exec_cli_with_eternus(self, exec_cmdline): if exec_cmdline == "show users": - ret = ('\r\nCLI> show users\r\n00\r\n' + ret = ('\r\nCLI> %s\r\n00\r\n' '3B\r\nf.ce\tMaintainer\t01\t00' '\t00\t00\r\ntestuser\tSoftware' - '\t01\t01\t00\t00\r\nCLI> ') - return ret + '\t01\t01\t00\t00\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('set volume-qos'): + ret = '%s\r\n00\r\n0001\r\nCLI> ' % exec_cmdline elif exec_cmdline.startswith('show volumes'): ret = ('\r\nCLI> %s\r\n00\r\n0560\r\n0000' '\tFJosv_0qJ4rpOHgFE8ipcJOMfBmg==' @@ -1083,14 +1256,69 @@ class FJISCSIDriverTestCase(test.TestCase): '\tFF\t20\tFF\tFFFF\t00' '\t600000E00D2A0000002A011500140000' '\t00\t00\tFF\tFF\tFFFFFFFF\t00' - '\t00\tFF\r\n0001\tFJosv_UkCZqMFZW3SU_JzxjHiKfg==' + '\t00\tFF\r\n0001\tFJosv_OgEZj1mSvKRvIKOExKktlg==' '\tA001\t0B\t00\t0000\tabcd1234_OSVD' '\t0000000000200000\t00\t00\t00000000' '\t0050\tFF\t00\tFF\tFF\t20\tFF\tFFFF' '\t00\t600000E00D2A0000002A0115001E0000' '\t00\t00\tFF\tFF\tFFFFFFFF\t00' '\t00\tFF' % exec_cmdline) - return ret + elif exec_cmdline.startswith('show enclosure-status'): + ret = ('\r\nCLI> %s\r\n00\r\n' + 'ETDX200S3_1\t01\tET203ACU\t4601417434\t280753\t20' + '\t00\t00\t01\t02\t01001000\tV10L87-9000\t91\r\n02' + '\r\n70000000\t30\r\nD0000100\t30\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('show volume-qos'): + ret = ('\r\nCLI> %s\r\n00\r\n' + '0002\t\r\n0000\tFJosv_0qJ4rpOHgFE8ipcJOMfBmg==\t0F' + '\t\r\n0001\tFJosv_OgEZj1mSvKRvIKOExKktlg==\t0D' + '\t\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('show qos-bandwidth-limit'): + ret = ('\r\nCLI> %s\r\n00\r\n0010\t\r\n00\t0000ffff\t0000ffff' + '\t0000ffff\t0000ffff\t0000ffff\t0000ffff\t0000ffff' + '\t0000ffff\t0000ffff\t0000ffff\t0000ffff\t0000ffff\r\n' + '01\t00000001\t00000001\t00000001\t00000001\t00000001' + '\t00000001\t00000001\t00000001\t00000001\t00000001' + '\t00000001\t00000001\r\n02\t00000002\t00000002\t00000002' + '\t00000002\t00000002\t00000002\t00000002\t00000002' + '\t00000002\t00000002\t00000002\t00000002\r\n03\t00000003' + '\t00000003\t00000003\t00000003\t00000003\t00000003' + '\t00000003\t00000003\t00000003\t00000003\t00000003' + '\t00000003\r\n04\t00000004\t00000004\t00000004\t00000004' + '\t00000004\t00000004\t00000004\t00000004\t00000004' + '\t00000004\t00000004\t00000004\r\n05\t00000005\t00000005' + '\t00000005\t00000005\t00000005\t00000005\t00000005' + '\t00000005\t00000005\t00000005\t00000005\t00000005\r\n06' + '\t00000006\t00000006\t00000006\t00000006\t00000006' + '\t00000006\t00000006\t00000006\t00000006\t00000006' + '\t00000006\t00000006\r\n07\t00000007\t00000007\t00000007' + '\t00000007\t00000007\t00000007\t00000007\t00000007' + '\t00000007\t00000007\t00000007\t00000007\r\n08\t00000008' + '\t00000008\t00000008\t00000008\t00000008\t00000008' + '\t00000008\t00000008\t00000008\t00000008\t00000008' + '\t00000008\r\n09\t00000009\t00000009\t00000009\t00000009' + '\t00000009\t00000009\t00000009\t00000009\t00000009' + '\t00000009\t00000009\t00000009\r\n0a\t0000000a\t0000000a' + '\t0000000a\t0000000a\t0000000a\t0000000a\t0000000a' + '\t0000000a\t0000000a\t0000000a\t0000000a\t0000000a\r\n0b' + '\t0000000b\t0000000b\t0000000b\t0000000b\t0000000b' + '\t0000000b\t0000000b\t0000000b\t0000000b\t0000000b' + '\t0000000b\t0000000b\r\n0c\t0000000c\t0000000c\t0000000c' + '\t0000000c\t0000000c\t0000000c\t0000000c\t0000000c' + '\t0000000c\t0000000c\t0000000c\t0000000c\r\n0d\t0000000d' + '\t0000000d\t0000000d\t0000000d\t0000000d\t0000000d' + '\t0000000d\t0000000d\t0000000d\t0000000d\t0000000d' + '\t0000000d\r\n0e\t0000000e\t0000000e\t0000000e\t0000000e' + '\t0000000e\t0000000e\t0000000e\t0000000e\t0000000e' + '\t0000000e\t0000000e\t0000000e\r\n0f\t0000000f\t0000000f' + '\t0000000f\t0000000f\t0000000f\t0000000f\t0000000f' + '\t0000000f\t0000000f\t0000000f\t0000000f\t0000000f' + '\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('set qos-bandwidth-limit'): + ret = '%s\r\n00\r\n0001\r\nCLI> ' % exec_cmdline + else: + ret = None + return ret def fake_safe_get(self, str=None): return str @@ -1199,3 +1427,388 @@ class FJISCSIDriverTestCase(test.TestCase): volume_info[key] = TEST_VOLUME[key] self.driver.extend_volume(volume_info, 10) + + def test_create_volume_with_qos(self): + self.driver.common._get_qos_specs = mock.Mock() + self.driver.common._get_qos_specs.return_value = {'maxBWS': '700'} + self.driver.common._set_qos = mock.Mock() + model_info = self.driver.create_volume(TEST_VOLUME_QOS) + self.assertEqual(FAKE_MODEL_INFO_QOS, model_info) + self.driver.common._set_qos.assert_called() + + +class FJCLITestCase(test.TestCase): + def __init__(self, *args, **kwargs): + super(FJCLITestCase, self).__init__(*args, **kwargs) + + def setUp(self): + super(FJCLITestCase, self).setUp() + self.mock_object(ssh_utils, 'SSHPool', mock.Mock()) + self.mock_object(eternus_dx_cli.FJDXCLI, '_exec_cli_with_eternus', + self.fake_exec_cli_with_eternus) + + cli = eternus_dx_cli.FJDXCLI(user=TEST_USER, + storage_ip=STORAGE_IP, + password=TEST_PASSWORD) + self.cli = cli + + def create_fake_options(self, **kwargs): + # Create options for CLI command. + FAKE_OPTION_DICT = {} + for key, value in kwargs.items(): + processed_key = key.replace('_', '-') + FAKE_OPTION_DICT[processed_key] = value + FAKE_OPTION = {**FAKE_OPTION_DICT} + return FAKE_OPTION + + def fake_exec_cli_with_eternus(self, exec_cmdline): + if exec_cmdline == "show users": + ret = ('\r\nCLI> %s\r\n00\r\n' + '3B\r\nf.ce\tMaintainer\t01\t00' + '\t00\t00\r\ntestuser\tSoftware' + '\t01\t01\t00\t00\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('set volume-qos'): + ret = '%s\r\n00\r\n0001\r\nCLI> ' % exec_cmdline + elif exec_cmdline.startswith('show volumes'): + ret = ('\r\nCLI> %s\r\n00\r\n0560\r\n0000' + '\tFJosv_0qJ4rpOHgFE8ipcJOMfBmg==' + '\tA001\t0B\t00\t0000\tabcd1234_TPP' + '\t0000000000200000\t00\t00' + '\t00000000\t0050\tFF\t00\tFF' + '\tFF\t20\tFF\tFFFF\t00' + '\t600000E00D2A0000002A011500140000' + '\t00\t00\tFF\tFF\tFFFFFFFF\t00' + '\t00\tFF\r\n0001\tFJosv_OgEZj1mSvKRvIKOExKktlg==' + '\tA001\t0B\t00\t0000\tabcd1234_OSVD' + '\t0000000000200000\t00\t00\t00000000' + '\t0050\tFF\t00\tFF\tFF\t20\tFF\tFFFF' + '\t00\t600000E00D2A0000002A0115001E0000' + '\t00\t00\tFF\tFF\tFFFFFFFF\t00' + '\t00\tFF' % exec_cmdline) + elif exec_cmdline.startswith('show enclosure-status'): + ret = ('\r\nCLI> %s\r\n00\r\n' + 'ETDX200S3_1\t01\tET203ACU\t4601417434\t280753\t20' + '\t00\t00\t01\t02\t01001000\tV10L87-9000\t91\r\n02' + '\r\n70000000\t30\r\nD0000100\t30\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('show volume-qos'): + ret = ('\r\nCLI> %s\r\n00\r\n' + '0001\r\n0000\tFJosv_0qJ4rpOHgFE8ipcJOMfBmg==\t01\t00\t00' + '\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('show qos-bandwidth-limit'): + ret = ('\r\nCLI> %s\r\n00\r\n0001\t\r\n00\t0000ffff\t0000ffff' + '\t0000ffff\t0000ffff\t0000ffff\t0000ffff\t0000ffff' + '\t0000ffff\t0000ffff\t0000ffff\t0000ffff\t0000ffff\r\n' + 'CLI> ' % exec_cmdline) + elif exec_cmdline.startswith('set qos-bandwidth-limit'): + ret = '%s\r\n00\r\n0001\r\nCLI> ' % exec_cmdline + elif exec_cmdline.startswith('delete volume'): + ret = '%s\r\n00\r\nCLI> ' % exec_cmdline + else: + ret = None + return ret + + @mock.patch.object(eternus_dx_cli.FJDXCLI, '_exec_cli_with_eternus') + def test_create_error_message(self, mock_exec_cli_with_eternus): + expected_error_value = {'message': ['-bandwidth-limit', 'asdf'], + 'rc': 'E8101', + 'result': 0} + + FAKE_VOLUME_NAME = 'FJosv_0qJ4rpOHgFE8ipcJOMfBmg==' + FAKE_BANDWIDTH_LIMIT = 'abcd' + FAKE_QOS_OPTION = self.create_fake_options( + volume_name=FAKE_VOLUME_NAME, + bandwidth_limit=FAKE_BANDWIDTH_LIMIT) + + error_cli_output = ('\r\nCLI> set volume-qos -volume-name %s ' + '-bandwidth-limit %s\r\n' + '01\r\n8101\r\n-bandwidth-limit\r\nasdf\r\n' + 'CLI> ' % (FAKE_VOLUME_NAME, FAKE_BANDWIDTH_LIMIT)) + mock_exec_cli_with_eternus.return_value = error_cli_output + + error_qos_output = self.cli._set_volume_qos(**FAKE_QOS_OPTION) + + self.assertEqual(expected_error_value, error_qos_output) + + def test_get_options(self): + expected_option = " -bandwidth-limit 2" + option = {"bandwidth-limit": 2} + ret = self.cli._get_option(**option) + self.assertEqual(expected_option, ret) + + def test_done_and_default_func(self): + # Test function 'done' and '_default_func' in CLI file. + self.cli.CMD_dic['check_user_role'] = mock.Mock() + self.cli._default_func = mock.Mock( + side_effect=Exception('Invalid function is specified')) + + cmd1 = 'check_user_role' + self.cli.done(cmd1) + self.cli.CMD_dic['check_user_role'].assert_called_with() + + cmd2 = 'test_run_cmd' + cli_ex = None + try: + self.cli.done(cmd2) + except Exception as ex: + cli_ex = ex + finally: + self.cli._default_func.assert_called() + self.assertEqual(str(cli_ex), "Invalid function is specified") + + def test_check_user_role(self): + FAKE_ROLE = {**FAKE_CLI_OUTPUT, 'message': 'Software'} + + role = self.cli._check_user_role() + self.assertEqual(FAKE_ROLE, role) + + def test_set_volume_qos(self): + FAKE_VOLUME_NAME = 'FJosv_0qJ4rpOHgFE8ipcJOMfBmg==' + FAKE_BANDWIDTH_LIMIT = 2 + FAKE_QOS_OPTION = self.create_fake_options( + volume_name=FAKE_VOLUME_NAME, + bandwidth_limit=FAKE_BANDWIDTH_LIMIT) + + FAKE_VOLUME_NUMBER = ['0001'] + FAKE_QOS_OUTPUT = {**FAKE_CLI_OUTPUT, 'message': FAKE_VOLUME_NUMBER} + + volume_number = self.cli._set_volume_qos(**FAKE_QOS_OPTION) + self.assertEqual(FAKE_QOS_OUTPUT, volume_number) + + def test_show_pool_provision(self): + FAKE_POOL_PROVIOSN_OPTION = self.create_fake_options( + pool_name='abcd1234_TPP') + + FAKE_PROVISION = {**FAKE_CLI_OUTPUT, 'message': FAKE_USEGB} + + proviosn = self.cli._show_pool_provision(**FAKE_POOL_PROVIOSN_OPTION) + self.assertEqual(FAKE_PROVISION, proviosn) + + def test_show_qos_bandwidth_limit(self): + FAKE_QOS_BANDWIDTH_LIMIT = {'read_bytes_sec': 65535, + 'read_iops_sec': 65535, + 'read_limit': 0, + 'total_bytes_sec': 65535, + 'total_iops_sec': 65535, + 'total_limit': 0, + 'write_bytes_sec': 65535, + 'write_iops_sec': 65535, + 'write_limit': 0} + FAKE_QOS_LIST = {**FAKE_CLI_OUTPUT, + 'message': [FAKE_QOS_BANDWIDTH_LIMIT]} + + qos_list = self.cli._show_qos_bandwidth_limit() + self.assertEqual(FAKE_QOS_LIST, qos_list) + + def test_set_qos_bandwidth_limit(self): + FAKE_VOLUME_NAME = 'FJosv_0qJ4rpOHgFE8ipcJOMfBmg==' + FAKE_READ_BANDWIDTH_LIMIT = 2 + FAKE_WRITE_BANDWIDTH_LIMIT = 3 + FAKE_QOS_OPTION = self.create_fake_options( + volume_name=FAKE_VOLUME_NAME, + read_bandwidth_limit=FAKE_READ_BANDWIDTH_LIMIT, + write_bandwidth_limit=FAKE_WRITE_BANDWIDTH_LIMIT) + + FAKE_VOLUME_NUMBER = ['0001'] + FAKE_QOS_OUTPUT = {**FAKE_CLI_OUTPUT, 'message': FAKE_VOLUME_NUMBER} + + volume_number = self.cli._set_qos_bandwidth_limit(**FAKE_QOS_OPTION) + self.assertEqual(FAKE_QOS_OUTPUT, volume_number) + + def test_show_volume_qos(self): + FAKE_VOLUME_QOS = {'total_limit': 1, + 'read_limit': 0, + 'write_limit': 0} + FAKE_VQOS_DATA_LIST = {**FAKE_CLI_OUTPUT, + 'message': [FAKE_VOLUME_QOS]} + + vqos_datalist = self.cli._show_volume_qos() + self.assertEqual(FAKE_VQOS_DATA_LIST, vqos_datalist) + + def test_show_enclosure_status(self): + FAKE_VERSION = 'V10L87-9000' + FAKE_VERSION_INFO = {**FAKE_CLI_OUTPUT, + 'message': {'version': FAKE_VERSION}} + + versioninfo = self.cli._show_enclosure_status() + self.assertEqual(FAKE_VERSION_INFO, versioninfo) + + def test_delete_volume(self): + FAKE_VOLUME_NAME = 'FJosv_0qJ4rpOHgFE8ipcJOMfBmg==' + FAKE_DELETE_OUTPUT = {**FAKE_CLI_OUTPUT, 'message': []} + FAKE_DELETE_VOLUME_OPTION = self.create_fake_options( + volume_name=FAKE_VOLUME_NAME) + + delete_output = self.cli._delete_volume(**FAKE_DELETE_VOLUME_OPTION) + self.assertEqual(FAKE_DELETE_OUTPUT, delete_output) + + +class FJCommonTestCase(test.TestCase): + def __init__(self, *args, **kwargs): + super(FJCommonTestCase, self).__init__(*args, **kwargs) + + def setUp(self): + super(FJCommonTestCase, self).setUp() + + # Make fake xml-configuration file. + self.config_file = tempfile.NamedTemporaryFile("w+", suffix='.xml') + self.addCleanup(self.config_file.close) + self.config_file.write(CONF) + self.config_file.flush() + + # Make fake Object by using mock as configuration object. + self.configuration = mock.Mock(spec=conf.Configuration) + self.configuration.cinder_eternus_config_file = self.config_file.name + self.configuration.safe_get = self.fake_safe_get + self.configuration.max_over_subscription_ratio = '20.0' + + self.mock_object(dx_common.FJDXCommon, '_get_eternus_connection', + self.fake_eternus_connection) + + instancename = FakeCIMInstanceName() + self.mock_object(dx_common.FJDXCommon, '_create_eternus_instance_name', + instancename.fake_create_eternus_instance_name) + + self.mock_object(ssh_utils, 'SSHPool', mock.Mock()) + + self.mock_object(dx_common.FJDXCommon, '_get_qos_specs', + return_value={}) + + self.mock_object(eternus_dx_cli.FJDXCLI, '_exec_cli_with_eternus', + self.fake_exec_cli_with_eternus) + # Set iscsi driver to self.driver. + driver = dx_iscsi.FJDXISCSIDriver(configuration=self.configuration) + self.driver = driver + + def fake_exec_cli_with_eternus(self, exec_cmdline): + if exec_cmdline == "show users": + ret = ('\r\nCLI> %s\r\n00\r\n' + '3B\r\nf.ce\tMaintainer\t01\t00' + '\t00\t00\r\ntestuser\tSoftware' + '\t01\t01\t00\t00\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('set volume-qos'): + ret = '%s\r\n00\r\n0001\r\nCLI> ' % exec_cmdline + elif exec_cmdline.startswith('show volumes'): + ret = ('\r\nCLI> %s\r\n00\r\n0560\r\n0000' + '\tFJosv_0qJ4rpOHgFE8ipcJOMfBmg==' + '\tA001\t0B\t00\t0000\tabcd1234_TPP' + '\t0000000000200000\t00\t00' + '\t00000000\t0050\tFF\t00\tFF' + '\tFF\t20\tFF\tFFFF\t00' + '\t600000E00D2A0000002A011500140000' + '\t00\t00\tFF\tFF\tFFFFFFFF\t00' + '\t00\tFF\r\n0001\tFJosv_OgEZj1mSvKRvIKOExKktlg==' + '\tA001\t0B\t00\t0000\tabcd1234_OSVD' + '\t0000000000200000\t00\t00\t00000000' + '\t0050\tFF\t00\tFF\tFF\t20\tFF\tFFFF' + '\t00\t600000E00D2A0000002A0115001E0000' + '\t00\t00\tFF\tFF\tFFFFFFFF\t00' + '\t00\tFF' % exec_cmdline) + elif exec_cmdline.startswith('show enclosure-status'): + ret = ('\r\nCLI> %s\r\n00\r\n' + 'ETDX200S3_1\t01\tET203ACU\t4601417434\t280753\t20' + '\t00\t00\t01\t02\t01001000\tV10L87-9000\t91\r\n02' + '\r\n70000000\t30\r\nD0000100\t30\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('show volume-qos'): + ret = ('\r\nCLI> %s\r\n00\r\n' + '0001\r\n0000\tFJosv_0qJ4rpOHgFE8ipcJOMfBmg==\t01\t00\t00' + '\r\nCLI> ' % exec_cmdline) + elif exec_cmdline.startswith('show qos-bandwidth-limit'): + ret = ('\r\nCLI> %s\r\n00\r\n0001\t\r\n00\t0000ffff\t0000ffff' + '\t0000ffff\t0000ffff\t0000ffff\t0000ffff\t0000ffff' + '\t0000ffff\t0000ffff\t0000ffff\t0000ffff\t0000ffff\r\n' + 'CLI> ' % exec_cmdline) + elif exec_cmdline.startswith('set qos-bandwidth-limit'): + ret = '\r\nCLI> %s\r\n00\r\n0001\r\nCLI> ' % exec_cmdline + else: + ret = None + return ret + + def fake_safe_get(self, str=None): + return str + + def fake_eternus_connection(self): + conn = FakeEternusConnection() + return conn + + def test_get_eternus_model(self): + ETERNUS_MODEL = self.driver.common._get_eternus_model() + self.assertEqual(3, ETERNUS_MODEL) + + def test_get_matadata(self): + TEST_METADATA = self.driver.common.get_metadata(TEST_VOLUME) + self.assertEqual({}, TEST_METADATA) + + def test_is_qos_or_format_support(self): + QOS_SUPPORT = \ + self.driver.common._is_qos_or_format_support('QOS setting') + self.assertTrue(QOS_SUPPORT) + + def test_get_qos_category_by_value(self): + FAKE_QOS_KEY = 'maxBWS' + FAKE_QOS_VALUE = 700 + FAKE_QOS_DICT = {'bandwidth-limit': 2} + QOS_Category_Dict = self.driver.common._get_qos_category_by_value( + FAKE_QOS_KEY, FAKE_QOS_VALUE) + self.assertEqual(FAKE_QOS_DICT, QOS_Category_Dict) + + def test_get_param(self): + FAKE_QOS_SPEC_DICT = {'total_bytes_sec': 2137152, + 'read_bytes_sec': 1068576, + 'unspport_key': 1234} + EXPECTED_KEY_DICT = {'read_bytes_sec': int(FAKE_QOS_SPEC_DICT + ['read_bytes_sec'] / + units.Mi), + 'read_iops_sec': MAX_IOPS, + 'total_bytes_sec': int(FAKE_QOS_SPEC_DICT + ['total_bytes_sec'] / + units.Mi), + 'total_iops_sec': MAX_IOPS} + KEY_DICT = self.driver.common._get_param(FAKE_QOS_SPEC_DICT) + self.assertEqual(EXPECTED_KEY_DICT, KEY_DICT) + + def test_check_iops(self): + FAKE_QOS_KEY = 'total_iops_sec' + FAKE_QOS_VALUE = 2137152 + QOS_VALUE = self.driver.common._check_iops(FAKE_QOS_KEY, + FAKE_QOS_VALUE) + self.assertEqual(FAKE_QOS_VALUE, QOS_VALUE) + + def test_check_throughput(self): + FAKE_QOS_KEY = 'total_bytes_sec' + FAKE_QOS_VALUE = 2137152 + QOS_VALUE = self.driver.common._check_throughput(FAKE_QOS_KEY, + FAKE_QOS_VALUE) + self.assertEqual(int(FAKE_QOS_VALUE / units.Mi), + QOS_VALUE) + + def test_get_qos_category(self): + FAKE_QOS_SPEC_DICT = {'total_bytes_sec': 2137152, + 'read_bytes_sec': 1068576} + FAKE_KEY_DICT = {'read_bytes_sec': int(FAKE_QOS_SPEC_DICT + ['read_bytes_sec'] / + units.Mi), + 'read_iops_sec': MAX_IOPS, + 'total_bytes_sec': int(FAKE_QOS_SPEC_DICT + ['total_bytes_sec'] / + units.Mi), + 'total_iops_sec': MAX_IOPS} + FAKE_RET_DICT = {'bandwidth-limit': FAKE_KEY_DICT['total_bytes_sec'], + 'read-bandwidth-limit': + FAKE_KEY_DICT['read_bytes_sec'], + 'write-bandwidth-limit': 0} + RET_DICT = self.driver.common._get_qos_category(FAKE_KEY_DICT) + self.assertEqual(FAKE_RET_DICT, RET_DICT) + + @mock.patch.object(eternus_dx_cli.FJDXCLI, '_exec_cli_with_eternus') + def test_set_limit(self, mock_exec_cli_with_eternus): + exec_cmdline = 'set qos-bandwidth-limit -mode volume-qos ' \ + '-bandwidth-limit 5 -iops 10000 -throughput 450' + mock_exec_cli_with_eternus.return_value = \ + '\r\nCLI> %s\r\n00\r\n0001\r\nCLI> ' % exec_cmdline + FAKE_MODE = 'volume-qos' + FAKE_LIMIT = 5 + FAKE_IOPS = 10000 + FAKE_THROUGHOUTPUT = 450 + self.driver.common._set_limit(FAKE_MODE, FAKE_LIMIT, + FAKE_IOPS, FAKE_THROUGHOUTPUT) + mock_exec_cli_with_eternus.assert_called_with(exec_cmdline) diff --git a/cinder/volume/drivers/fujitsu/eternus_dx/constants.py b/cinder/volume/drivers/fujitsu/eternus_dx/constants.py index 11b05e776d1..8ca17a04bff 100644 --- a/cinder/volume/drivers/fujitsu/eternus_dx/constants.py +++ b/cinder/volume/drivers/fujitsu/eternus_dx/constants.py @@ -22,6 +22,8 @@ RETURN_TO_RESOURCEPOOL = 19 DETACH = 8 BROKEN = 5 +DX_S2 = 2 +DX_S3 = 3 JOB_RETRIES = 60 JOB_INTERVAL_SEC = 10 TIMES_MIN = 3 @@ -40,6 +42,15 @@ STOR_CONF = "FUJITSU_StorageConfigurationService" CTRL_CONF = "FUJITSU_ControllerConfigurationService" UNDEF_MSG = 'Undefined Error!!' +MAX_IOPS = 4294967295 +MAX_THROUGHPUT = 2097151 +MIN_IOPS = 1 +MIN_THROUGHPUT = 1 + +QOS_VERSION = 'V11L30-0000' +# Here is a misspelling, and the right value should be "Thinprovisioning_POOL". +# It would not be compatible with the metadata of the legacy volumes, +# so this spelling mistake needs to be retained. POOL_TYPE_dic = { RAIDGROUP: 'RAID_GROUP', TPPOOL: 'Thinporvisioning_POOL', @@ -53,6 +64,19 @@ OPERATION_dic = { OPC: DETACH, EC_REC: DETACH, } +FJ_QOS_KEY_list = [ + 'maxBWS' +] +FJ_QOS_KEY_BYTES_list = [ + 'read_bytes_sec', + 'write_bytes_sec', + 'total_bytes_sec' +] +FJ_QOS_KEY_IOPS_list = [ + 'read_iops_sec', + 'write_iops_sec', + 'total_iops_sec' +] RETCODE_dic = { '0': 'Success', diff --git a/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_cli.py b/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_cli.py index bd7124b6d8c..a9054bbb464 100644 --- a/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_cli.py +++ b/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_cli.py @@ -15,6 +15,7 @@ # """Cinder Volume driver for Fujitsu ETERNUS DX S3 series.""" + import six from cinder.i18n import _ @@ -47,6 +48,12 @@ class FJDXCLI(object): self.CMD_dic = { 'check_user_role': self._check_user_role, 'show_pool_provision': self._show_pool_provision, + 'show_qos_bandwidth_limit': self._show_qos_bandwidth_limit, + 'set_qos_bandwidth_limit': self._set_qos_bandwidth_limit, + 'set_volume_qos': self._set_volume_qos, + 'show_volume_qos': self._show_volume_qos, + 'show_enclosure_status': self._show_enclosure_status, + 'delete_volume': self._delete_volume } self.SMIS_dic = { @@ -129,7 +136,7 @@ class FJDXCLI(object): stdoutdata = '' while True: temp = chan.recv(65535) - if isinstance(temp, six.binary_type): + if isinstance(temp, bytes): temp = temp.decode('utf-8') else: temp = str(temp) @@ -143,7 +150,7 @@ class FJDXCLI(object): break except Exception as e: raise Exception(_("Execute CLI " - "command error. Error: %s") % six.text_type(e)) + "command error. Error: %s") % e) finally: if ssh: self.ssh_pool.put(ssh) @@ -188,7 +195,7 @@ class FJDXCLI(object): def _get_option(**option): """Create option strings from dictionary.""" ret = "" - for key, value in six.iteritems(option): + for key, value in option.items(): ret += " -%(key)s %(value)s" % {'key': key, 'value': value} return ret @@ -232,6 +239,10 @@ class FJDXCLI(object): } return output + def _set_volume_qos(self, **option): + """Exec set volume-qos.""" + return self._exec_cli("set volume-qos", **option) + def _show_pool_provision(self, **option): """Get TPP provision capacity information.""" try: @@ -257,8 +268,129 @@ class FJDXCLI(object): output = { 'result': 0, 'rc': '4', - 'message': "show pool provision capacity error: %s" - % six.text_type(ex) + 'message': "show pool provision capacity error: %s" % ex } return output + + def _show_qos_bandwidth_limit(self, **option): + """Get qos bandwidth limit.""" + clidata = None + try: + output = self._exec_cli("show qos-bandwidth-limit", **option) + + # return error + rc = output['rc'] + + if rc != "0": + return output + + qoslist = [] + clidatalist = output.get('message') + + for clidataline in clidatalist[1:]: + clidata = clidataline.split('\t') + qoslist.append({'total_limit': int(clidata[0], 16), + 'total_iops_sec': int(clidata[1], 16), + 'total_bytes_sec': int(clidata[2], 16), + 'read_limit': int(clidata[0], 16), + 'read_iops_sec': int(clidata[3], 16), + 'read_bytes_sec': int(clidata[4], 16), + 'write_limit': int(clidata[0], 16), + 'write_iops_sec': int(clidata[5], 16), + 'write_bytes_sec': int(clidata[6], 16)}) + + output['message'] = qoslist + + except IndexError as ex: + msg = ('The results returned by cli are not as expected. ' + 'Exception string: %s' % clidata) + output = {'result': 0, + 'rc': '4', + 'message': "Show qos bandwidth limit error: %s. %s" + % (ex, msg)} + + except Exception as ex: + output = {'result': 0, + 'rc': '4', + 'message': "Show qos bandwidth limit error: %s" % ex} + + return output + + def _set_qos_bandwidth_limit(self, **option): + """Set qos bandwidth limit""" + return self._exec_cli("set qos-bandwidth-limit", **option) + + def _show_volume_qos(self, **option): + """Get volumes with qos.""" + clidata = None + try: + output = self._exec_cli("show volume-qos", **option) + + # return error + rc = output['rc'] + + if rc != "0": + return output + + vqosdatalist = [] + clidatalist = output.get('message') + + for clidataline in clidatalist[1:]: + clidata = clidataline.split('\t') + vqosdatalist.append({'total_limit': int(clidata[2], 16), + 'read_limit': int(clidata[3], 16), + 'write_limit': int(clidata[4], 16)}) + + output['message'] = vqosdatalist + + except IndexError as ex: + msg = ('The results returned by cli are not as expected. ' + 'Exception string: %s' % clidata) + output = {'result': 0, + 'rc': '4', + 'message': "Show volume qos error: %s. %s" % (ex, msg)} + + except Exception as ex: + output = {'result': 0, + 'rc': '4', + 'message': "Show volume qos error: %s" % ex} + + return output + + def _show_enclosure_status(self, **option): + """Get the version of machine.""" + clidata = None + try: + output = self._exec_cli("show enclosure-status", **option) + + # return error + rc = output['rc'] + + if rc != "0": + return output + + clidatalist = output.get('message') + clidata = clidatalist[0].split('\t') + versioninfo = {'version': clidata[11]} + + output['message'] = versioninfo + + except IndexError as ex: + msg = ('The results returned by cli are not as expected. ' + 'Exception string: %s' % clidata) + output = {'result': 0, + 'rc': '4', + 'message': "Show enclosure status error: %s. %s" + % (ex, msg)} + + except Exception as ex: + output = {'result': 0, + 'rc': '4', + 'message': "Show enclosure status error: %s" % ex} + + return output + + def _delete_volume(self, **option): + """Exec delete volume.""" + return self._exec_cli('delete volume', **option) diff --git a/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_common.py b/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_common.py index 2efa5de5c49..4160004769d 100644 --- a/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_common.py +++ b/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_common.py @@ -18,7 +18,6 @@ """Cinder Volume driver for Fujitsu ETERNUS DX S3 series.""" -import ast import base64 import time @@ -31,12 +30,15 @@ from oslo_utils.secretutils import md5 from oslo_utils import units import six +from cinder import context from cinder import exception from cinder.i18n import _ from cinder import utils from cinder.volume import configuration as conf from cinder.volume.drivers.fujitsu.eternus_dx import constants as CONSTANTS from cinder.volume.drivers.fujitsu.eternus_dx import eternus_dx_cli +from cinder.volume import qos_specs +from cinder.volume import volume_types from cinder.volume import volume_utils LOG = logging.getLogger(__name__) @@ -58,14 +60,22 @@ CONF.register_opts(FJ_ETERNUS_DX_OPT_opts, group=conf.SHARED_CONF_GROUP) class FJDXCommon(object): - """Common code that does not depend on protocol.""" + """Common code that does not depend on protocol. - VERSION = "1.3.0" + Version history: + + 1.0 - Initial driver + 1.3.0 - Community base version + 1.4.0 - Add support for QoS. + + """ + + VERSION = "1.4.0" stats = { 'driver_version': VERSION, 'storage_protocol': None, 'vendor_name': 'FUJITSU', - 'QoS_support': False, + 'QoS_support': True, 'volume_backend_name': None, } @@ -77,12 +87,9 @@ class FJDXCommon(object): self.configuration = configuration self.configuration.append_config_values(FJ_ETERNUS_DX_OPT_opts) - if prtcl == 'iSCSI': - # Get iSCSI ipaddress from driver configuration file. - self.configuration.iscsi_ip_address = ( - self._get_drvcfg('EternusISCSIIP')) self.conn = None self.fjdxcli = {} + self.model_name = self._get_eternus_model() self._check_user() @staticmethod @@ -95,68 +102,118 @@ class FJDXCommon(object): 'volume id: %(vid)s, volume size: %(vsize)s.', {'vid': volume['id'], 'vsize': volume['size']}) + d_metadata = self.get_metadata(volume) + + element_path, metadata = self._create_volume(volume) + + d_metadata.update(metadata) + + model_update = { + 'provider_location': str(element_path), + 'metadata': d_metadata + } + + # Set qos to created volume. + try: + self._set_qos(volume, use_id=True) + except Exception as ex: + LOG.error('create_volume, ' + 'error occurred while setting volume qos. ' + 'Error information: %s', ex) + # While set qos failed, delete volume from backend + volumename = metadata['FJ_Volume_Name'] + self._delete_volume_after_error(volumename) + + return model_update + + def _create_volume(self, volume): + LOG.debug('_create_volume, ' + 'volume id: %(vid)s, volume size: %(vsize)s.', + {'vid': volume['id'], 'vsize': volume['size']}) + self.conn = self._get_eternus_connection() volumesize = int(volume['size']) * units.Gi - volumename = self._create_volume_name(volume['id']) + volumename = self._get_volume_name(volume, use_id=True) - LOG.debug('create_volume, volumename: %(volumename)s, ' + LOG.debug('_create_volume, volumename: %(volumename)s, ' 'volumesize: %(volumesize)u.', {'volumename': volumename, 'volumesize': volumesize}) - # get poolname from driver configuration file - eternus_pool = volume_utils.extract_host(volume['host'], 'pool') - # Existence check the pool - pool = self._find_pool(eternus_pool) - - if 'RSP' in pool['InstanceID']: - pooltype = CONSTANTS.RAIDGROUP - else: - pooltype = CONSTANTS.TPPOOL - configservice = self._find_eternus_service(CONSTANTS.STOR_CONF) - if configservice is None: - msg = (_('create_volume, volume: %(volume)s, ' + if not configservice: + msg = (_('_create_volume, volume: %(volume)s, ' 'volumename: %(volumename)s, ' 'eternus_pool: %(eternus_pool)s, ' 'Storage Configuration Service not found.') % {'volume': volume, - 'volumename': volumename, - 'eternus_pool': eternus_pool}) + 'volumename': volumename}) LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) - LOG.debug('create_volume, ' - 'CreateOrModifyElementFromStoragePool, ' - 'ConfigService: %(service)s, ' - 'ElementName: %(volumename)s, ' - 'InPool: %(eternus_pool)s, ' - 'ElementType: %(pooltype)u, ' - 'Size: %(volumesize)u.', - {'service': configservice, - 'volumename': volumename, - 'eternus_pool': eternus_pool, - 'pooltype': pooltype, - 'volumesize': volumesize}) + # Get all pools information on ETERNUS. + pools_instance_list = self._find_all_pools_instances(self.conn) - # Invoke method for create volume - rc, errordesc, job = self._exec_eternus_service( - 'CreateOrModifyElementFromStoragePool', - configservice, - ElementName=volumename, - InPool=pool, - ElementType=self._pywbem_uint(pooltype, '16'), - Size=self._pywbem_uint(volumesize, '64')) + if 'host' in volume: + eternus_pool = volume_utils.extract_host(volume['host'], 'pool') - if rc == CONSTANTS.VOLUMENAME_IN_USE: # Element Name is in use - LOG.warning('create_volume, ' + for pool, ptype in pools_instance_list: + if eternus_pool == pool['ElementName']: + pool_instance = pool + if ptype == 'RAID': + pooltype = CONSTANTS.RAIDGROUP + else: + pooltype = CONSTANTS.TPPOOL + break + else: + msg = (_('_create_volume, volume: %(volume)s, ' + 'volumename: %(volumename)s, ' + 'poolname: %(poolname)s, ' + 'Cannot find this pool on ETERNUS.') + % {'volume': volume, + 'volumename': volumename, + 'poolname': eternus_pool}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + LOG.debug('_create_volume, ' + 'CreateOrModifyElementFromStoragePool, ' + 'ConfigService: %(service)s, ' + 'ElementName: %(volumename)s, ' + 'InPool: %(eternus_pool)s, ' + 'ElementType: %(pooltype)u, ' + 'Size: %(volumesize)u.', + {'service': configservice, + 'volumename': volumename, + 'eternus_pool': eternus_pool, + 'pooltype': pooltype, + 'volumesize': volumesize}) + + # Invoke method for create volume. + rc, errordesc, job = self._exec_eternus_service( + 'CreateOrModifyElementFromStoragePool', + configservice, + ElementName=volumename, + InPool=pool_instance.path, + ElementType=self._pywbem_uint(pooltype, '16'), + Size=self._pywbem_uint(volumesize, '64')) + + else: + msg = (_('create_volume, volume id: %(vid)s, ' + 'volume size: %(vsize)s, ' + 'Cannot find volume host.') + % {'vid': volume['id'], 'vsize': volume['size']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + if rc == CONSTANTS.VOLUMENAME_IN_USE: # Element Name is in use. + LOG.warning('_create_volume, ' 'volumename: %(volumename)s, ' 'Element Name is in use.', {'volumename': volumename}) - vol_instance = self._find_lun(volume) - element = vol_instance + element = self._find_lun(volume) elif rc != 0: - msg = (_('create_volume, ' + msg = (_('_create_volume, ' 'volumename: %(volumename)s, ' 'poolname: %(eternus_pool)s, ' 'Return code: %(rc)lu, ' @@ -170,12 +227,13 @@ class FJDXCommon(object): else: element = job['TheElement'] - # Get eternus model name + # Get eternus model name. try: - systemnamelist = ( - self._enum_eternus_instances('FUJITSU_StorageProduct')) + systemnamelist = self._enum_eternus_instances( + 'FUJITSU_StorageProduct', + conn=self.conn) except Exception: - msg = (_('create_volume, ' + msg = (_('_create_volume, ' 'volume: %(volume)s, ' 'EnumerateInstances, ' 'cannot connect to ETERNUS.') @@ -183,16 +241,12 @@ class FJDXCommon(object): LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) - LOG.debug('create_volume, ' + LOG.debug('_create_volume, ' 'volumename: %(volumename)s, ' - 'Return code: %(rc)lu, ' - 'Error: %(errordesc)s, ' 'Backend: %(backend)s, ' 'Pool Name: %(eternus_pool)s, ' 'Pool Type: %(pooltype)s.', {'volumename': volumename, - 'rc': rc, - 'errordesc': errordesc, 'backend': systemnamelist[0]['IdentifyingNumber'], 'eternus_pool': eternus_pool, 'pooltype': CONSTANTS.POOL_TYPE_dic[pooltype]}) @@ -201,22 +255,22 @@ class FJDXCommon(object): element_path = { 'classname': element.classname, 'keybindings': { - 'CreationClassName': element['CreationClassName'], 'SystemName': element['SystemName'], 'DeviceID': element['DeviceID'], - 'SystemCreationClassName': element['SystemCreationClassName'] - } + }, + 'vol_name': volumename, } volume_no = "0x" + element['DeviceID'][24:28] + metadata = { + 'FJ_Backend': systemnamelist[0]['IdentifyingNumber'], + 'FJ_Volume_Name': volumename, + 'FJ_Volume_No': volume_no, + 'FJ_Pool_Name': eternus_pool, + 'FJ_Pool_Type': CONSTANTS.POOL_TYPE_dic[pooltype], + } - metadata = {'FJ_Backend': systemnamelist[0]['IdentifyingNumber'], - 'FJ_Volume_Name': volumename, - 'FJ_Volume_No': volume_no, - 'FJ_Pool_Name': eternus_pool, - 'FJ_Pool_Type': CONSTANTS.POOL_TYPE_dic[pooltype]} - - return (element_path, metadata) + return element_path, metadata def create_pool_info(self, pool_instance, volume_count, pool_type): """Create pool information from pool instance.""" @@ -280,9 +334,11 @@ class FJDXCommon(object): raise exception.VolumeBackendAPIException(data=msg) # Create volume for the target volume. - (element_path, metadata) = self.create_volume(volume) + model_update = self.create_volume(volume) + element_path = eval(model_update.get('provider_location')) + metadata = model_update.get('metadata') target_volume_instancename = self._create_eternus_instance_name( - element_path['classname'], element_path['keybindings']) + element_path['classname'], element_path['keybindings'].copy()) try: target_volume_instance = ( @@ -316,9 +372,11 @@ class FJDXCommon(object): LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) - (element_path, metadata) = self.create_volume(volume) + model_update = self.create_volume(volume) + element_path = eval(model_update.get('provider_location')) + metadata = model_update.get('metadata') target_volume_instancename = self._create_eternus_instance_name( - element_path['classname'], element_path['keybindings']) + element_path['classname'], element_path['keybindings'].copy()) try: target_volume_instance = ( @@ -413,7 +471,7 @@ class FJDXCommon(object): LOG.debug('_delete_volume_setting, volume id: %s.', volume['id']) # Check the existence of volume. - volumename = self._create_volume_name(volume['id']) + volumename = self._get_volume_name(volume) vol_instance = self._find_lun(volume) if vol_instance is None: @@ -508,6 +566,31 @@ class FJDXCommon(object): 'rc': rc, 'errordesc': errordesc}) + def _delete_volume_after_error(self, volumename): + # If error occures while set qos after create a volume,then delete + # the created volume. + LOG.debug('_delete_volume_after_error, ' + 'volume name: %(volumename)s.', + {'volumename': volumename}) + + param_dict = {'volume-name': volumename} + rc, errordesc, data = self._exec_eternus_cli( + 'delete_volume', + **param_dict) + + if rc == 0: + msg = (_('_delete_volume_after_error, ' + 'volumename: %(volumename)s, ' + 'Delete Successed.') + % {'volumename': volumename}) + else: + msg = (_('_delete_volume_after_error, ' + 'volumename: %(volumename)s, ' + 'Delete Failed.') + % {'volumename': volumename}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + @lockutils.synchronized('ETERNUS-vol', 'cinder-', True) def create_snapshot(self, snapshot): """Create snapshot using SnapOPC.""" @@ -518,10 +601,9 @@ class FJDXCommon(object): self.conn = self._get_eternus_connection() snapshotname = snapshot['name'] volumename = snapshot['volume_name'] - vol_id = snapshot['volume_id'] volume = snapshot['volume'] - d_volumename = self._create_volume_name(snapshot['id']) - s_volumename = self._create_volume_name(vol_id) + d_volumename = self._get_volume_name(snapshot, use_id=True) + s_volumename = self._get_volume_name(volume) vol_instance = self._find_lun(volume) repservice = self._find_eternus_service(CONSTANTS.REPL) @@ -610,11 +692,10 @@ class FJDXCommon(object): element_path = { 'classname': element.classname, 'keybindings': { - 'CreationClassName': element['CreationClassName'], 'SystemName': element['SystemName'], 'DeviceID': element['DeviceID'], - 'SystemCreationClassName': element['SystemCreationClassName'] - } + }, + 'vol_name': d_volumename, } sdv_no = "0x" + element['DeviceID'][24:28] @@ -741,7 +822,7 @@ class FJDXCommon(object): self.conn = self._get_eternus_connection() volumesize = new_size * units.Gi - volumename = self._create_volume_name(volume['id']) + volumename = self._get_volume_name(volume) # Get source volume instance. vol_instance = self._find_lun(volume) @@ -1093,24 +1174,34 @@ class FJDXCommon(object): LOG.debug('_get_eternus_connection, conn: %s.', conn) return conn - def _create_volume_name(self, id_code): - """create volume_name on ETERNUS from id on OpenStack.""" - LOG.debug('_create_volume_name, id_code: %s.', id_code) + def _get_volume_name(self, volume, use_id=False): + """Get volume_name on ETERNUS from volume on OpenStack.""" + LOG.debug('_get_volume_name, volume_id: %s.', volume['id']) - if id_code is None: - msg = _('_create_volume_name, id_code is None.') - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) + if (not use_id and 'provider_location' in volume and + volume['provider_location']): + location = eval(volume['provider_location']) + if 'vol_name' in location: + LOG.debug('_get_volume_name, by provider_location, ' + 'vol_name: %s.', location['vol_name']) + return location['vol_name'] + + id_code = volume['id'] m = md5(usedforsecurity=False) m.update(id_code.encode('utf-8')) - # pylint: disable=E1121 + # Pylint: disable=E1121. volumename = base64.urlsafe_b64encode(m.digest()).decode() - ret = CONSTANTS.VOL_PREFIX + six.text_type(volumename) + vol_name = CONSTANTS.VOL_PREFIX + six.text_type(volumename) - LOG.debug('_create_volume_name, ret: %s', ret) - return ret + if self.model_name == CONSTANTS.DX_S2: + LOG.debug('_get_volume_name, volume name is 16 digit.') + vol_name = vol_name[:16] + + LOG.debug('_get_volume_name, by volume id, ' + 'vol_name: %s.', vol_name) + return vol_name def _find_pool(self, eternus_pool, detail=False): """find Instance or InstanceName of pool by pool name on ETERNUS.""" @@ -1156,6 +1247,30 @@ class FJDXCommon(object): LOG.debug('_find_pool, pool: %s.', ret) return ret + def _find_all_pools_instances(self, conn): + LOG.debug('_find_all_pools_instances, conn: %s', conn) + + try: + tppoollist = self._enum_eternus_instances( + 'FUJITSU_ThinProvisioningPool', conn=conn) + rgpoollist = self._enum_eternus_instances( + 'FUJITSU_RAIDStoragePool', conn=conn) + except Exception: + msg = _('_find_pool, ' + 'eternus_pool:%(eternus_pool)s, ' + 'EnumerateInstances, ' + 'cannot connect to ETERNUS.') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + # Make total pools list. + tppools = [(tppool, 'TPP') for tppool in tppoollist] + rgpools = [(rgpool, 'RAID') for rgpool in rgpoollist] + poollist = tppools + rgpools + + LOG.debug('_find_all_pools_instances, poollist: %s', len(poollist)) + return poollist + def _find_pools(self, poolname_list, conn): """Find Instance or InstanceName of pool by pool name on ETERNUS.""" LOG.debug('_find_pool, pool name: %s.', poolname_list) @@ -1164,24 +1279,7 @@ class FJDXCommon(object): pools = [] # Get pools info from CIM instance(include info about instance path). - try: - tppoollist = self._enum_eternus_instances( - 'FUJITSU_ThinProvisioningPool', conn=conn) - rgpoollist = self._enum_eternus_instances( - 'FUJITSU_RAIDStoragePool', conn=conn) - except Exception: - msg = (_('_find_pool, ' - 'eternus_pool:%(eternus_pool)s, ' - 'EnumerateInstances, ' - 'cannot connect to ETERNUS.') - % {'eternus_pool': target_poolname}) - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - - # Make total pools list. - tppools = [(tppool, 'TPP') for tppool in tppoollist] - rgpools = [(rgpool, 'RAID') for rgpool in rgpoollist] - poollist = tppools + rgpools + poollist = self._find_all_pools_instances(conn) # One eternus backend has only one special pool name # so just use pool name can get the target pool. @@ -1396,13 +1494,21 @@ class FJDXCommon(object): @lockutils.synchronized('ETERNUS-SMIS-getinstance', 'cinder-', True) @utils.retry(exception.VolumeBackendAPIException) - def _get_eternus_instance(self, classname, **param_dict): + def _get_eternus_instance(self, classname, allownone=False, **param_dict): """Get Instance.""" LOG.debug('_get_eternus_instance, ' 'classname: %(cls)s, param: %(param)s.', {'cls': classname, 'param': param_dict}) - ret = self.conn.GetInstance(classname, **param_dict) + ret = None + try: + ret = self.conn.GetInstance(classname, **param_dict) + except Exception as e: + if e.args[0] == 6 and allownone: + return ret + else: + msg = _('_get_eternus_instance, Error:%s.') % e + raise exception.VolumeBackendAPIException(data=msg) LOG.debug('_get_eternus_instance, ret: %s.', ret) return ret @@ -1458,7 +1564,8 @@ class FJDXCommon(object): 'classname: %(cls)s, bindings: %(bind)s.', {'cls': classname, 'bind': bindings}) - instancename = None + bindings['CreationClassName'] = classname + bindings['SystemCreationClassName'] = 'FUJITSU_StorageComputerSystem' try: instancename = pywbem.CIMInstanceName( @@ -1472,13 +1579,13 @@ class FJDXCommon(object): return instancename def _find_lun(self, volume): - """find lun instance from volume class or volumename on ETERNUS.""" + """Find lun instance from volume class or volumename on ETERNUS.""" LOG.debug('_find_lun, volume id: %s.', volume['id']) volumeinstance = None - volumename = self._create_volume_name(volume['id']) + volumename = self._get_volume_name(volume) try: - location = ast.literal_eval(volume['provider_location']) + location = eval(volume['provider_location']) classname = location['classname'] bindings = location['keybindings'] @@ -1495,10 +1602,10 @@ class FJDXCommon(object): 'volume_insatnce_name: %(volume_instance_name)s.', {'volume_instance_name': volume_instance_name}) - vol_instance = ( - self._get_eternus_instance(volume_instance_name)) + vol_instance = self._get_eternus_instance(volume_instance_name, + allownone=True,) - if vol_instance['ElementName'] == volumename: + if vol_instance and vol_instance['ElementName'] == volumename: volumeinstance = vol_instance except Exception: volumeinstance = None @@ -1506,47 +1613,17 @@ class FJDXCommon(object): 'Cannot get volume instance from provider location, ' 'Search all volume using EnumerateInstanceNames.') - if volumeinstance is None: - # for old version - + if not volumeinstance: + # For old version. LOG.debug('_find_lun, ' 'volumename: %(volumename)s.', {'volumename': volumename}) - # get volume instance from volumename on ETERNUS - try: - namelist = self._enum_eternus_instance_names( - 'FUJITSU_StorageVolume') - except Exception: - msg = (_('_find_lun, ' - 'volumename: %(volumename)s, ' - 'EnumerateInstanceNames, ' - 'cannot connect to ETERNUS.') - % {'volumename': volumename}) - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - - for name in namelist: - try: - vol_instance = self._get_eternus_instance(name) - - if vol_instance['ElementName'] == volumename: - volumeinstance = vol_instance - path = volumeinstance.path - - LOG.debug('_find_lun, ' - 'volumename: %(volumename)s, ' - 'vol_instance: %(vol_instance)s.', - {'volumename': volumename, - 'vol_instance': path}) - break - except Exception: - continue - else: - LOG.debug('_find_lun, ' - 'volumename: %(volumename)s, ' - 'volume not found on ETERNUS.', - {'volumename': volumename}) + vol_name = { + 'source-name': volumename + } + # Get volume instance from volumename on ETERNUS. + volumeinstance = self._find_lun_with_listup(**vol_name) LOG.debug('_find_lun, ret: %s.', volumeinstance) return volumeinstance @@ -1977,7 +2054,7 @@ class FJDXCommon(object): {'vid': volume['id'], 'connector': connector, 'frc': force}) - volumename = self._create_volume_name(volume['id']) + volumename = self._get_volume_name(volume) vol_instance = self._find_lun(volume) if vol_instance is None: LOG.info('_unmap_lun, ' @@ -2261,6 +2338,86 @@ class FJDXCommon(object): return result + def _find_lun_with_listup(self, conn=None, **kwargs): + """Find lun instance with source name or source id on ETERNUS.""" + LOG.debug('_find_lun_with_listup start.') + + volumeinstance = None + src_id = kwargs.get('source-id', None) + src_name = kwargs.get('source-name', None) + + if not src_id and not src_name: + msg = (_('_find_lun_with_listup, ' + 'source-name or source-id: %s, ' + 'Must specify source-name or source-id.') + % kwargs) + LOG.error(msg) + raise exception.ManageExistingInvalidReference(data=msg) + + if src_id and src_name: + msg = (_('_find_lun_with_listup, ' + 'source-name or source-id: %s, ' + 'Must only specify source-name or source-id.') + % kwargs) + LOG.error(msg) + raise exception.ManageExistingInvalidReference(data=msg) + + if src_id and not src_id.isdigit(): + msg = (_('_find_lun_with_listup, ' + 'the specified source-id(%s) must be a decimal number.') + % src_id) + LOG.error(msg) + raise exception.ManageExistingInvalidReference(data=msg) + + # Get volume instance by volumename or volumeno on ETERNUS. + try: + propertylist = [ + 'SystemName', + 'DeviceID', + 'ElementName', + 'Purpose', + 'BlockSize', + 'NumberOfBlocks', + 'Name', + 'OtherUsageDescription', + 'IsCompressed', + 'IsDeduplicated' + ] + vollist = self._enum_eternus_instances( + 'FUJITSU_StorageVolume', + conn=conn, + PropertyList=propertylist) + except Exception: + msg = (_('_find_lun_with_listup, ' + 'source-name or source-id: %s, ' + 'EnumerateVolumeInstance.') + % kwargs) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + for vol_instance in vollist: + if src_id: + volume_no = "0x" + vol_instance['DeviceID'][24:28] + try: + # Skip hidden tppv volumes. + if int(src_id) == int(volume_no, 16): + volumeinstance = vol_instance + break + except ValueError: + continue + if src_name: + if vol_instance['ElementName'] == src_name: + volumeinstance = vol_instance + break + else: + LOG.debug('_find_lun_with_listup, ' + 'source-name or source-id: %s, ' + 'volume not found on ETERNUS.', kwargs) + + LOG.debug('_find_lun_with_listup end, ' + 'volume instance: %s.', volumeinstance) + return volumeinstance + def _find_pool_from_volume(self, vol_instance, manage_type='volume'): """Find Instance or InstanceName of pool by volume instance.""" LOG.debug('_find_pool_from_volume, volume: %(volume)s.', @@ -2324,6 +2481,32 @@ class FJDXCommon(object): {'poolname': poolname, 'target_pool': target_pool}) return poolname, target_pool + def _get_eternus_model(self): + """Get ENTERNUS model.""" + self.conn = self._get_eternus_connection() + ret = CONSTANTS.DX_S3 + try: + systemnamelist = self._enum_eternus_instances( + 'FUJITSU_StorageProduct', conn=self.conn) + except Exception: + msg = _('_get_eternus_model, EnumerateInstances, ' + 'cannot connect to ETERNUS.') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + systemname = systemnamelist[0]['IdentifyingNumber'] + + LOG.debug('_get_eternus_model, ' + 'systemname: %(systemname)s, ' + 'storage is DX S%(model)s.', + {'systemname': systemname, + 'model': systemname[4]}) + + if six.text_type(systemname[4]) == '2': + ret = CONSTANTS.DX_S2 + + return ret + def _check_user(self): """Check whether user's role is accessible to ETERNUS and Software.""" ret = True @@ -2500,3 +2683,422 @@ class FJDXCommon(object): 'rc': rc, 'errordesc': errordesc}) return ret + + @staticmethod + def get_metadata(volume): + """Get metadata using volume information.""" + LOG.debug('get_metadata, volume id: %s.', + volume['id']) + + d_metadata = {} + + metadata = volume.get('volume_metadata') + + # value={} enters the if branch, value=None enters the else. + if metadata is not None: + d_metadata = { + data['key']: data['value'] for data in metadata + } + else: + metadata = volume.get('metadata') + if metadata: + d_metadata = { + key: metadata[key] for key in metadata + } + + LOG.debug('get_metadata, metadata is: %s.', d_metadata) + return d_metadata + + def _set_qos(self, volume, use_id=False): + """Set volume qos using ETERNUS CLI.""" + LOG.debug('_set_qos, volumeid: %(volumeid)s.', + {'volumeid': volume['id']}) + + qos_support = self._is_qos_or_format_support('QOS setting') + # Storage is DX S2 series, qos is not supported. + if not qos_support: + return + + qos_specs_dict = self._get_qos_specs(volume) + if not qos_specs_dict: + # Can not get anything from 'qos_specs_id'. + return + + # Get storage version information. + rc, emsg, clidata = self._exec_eternus_cli('show_enclosure_status') + if rc != 0: + msg = (_('_set_qos, ' + 'show_enclosure_status failed. ' + 'Return code: %(rc)lu, ' + 'Error: %(errormsg)s, ' + 'Message: %(clidata)s.') + % {'rc': rc, + 'errormsg': emsg, + 'clidata': clidata}) + LOG.warning(msg) + raise exception.VolumeBackendAPIException(data=msg) + + category_dict = {} + unsupport = [] + + # If storage version is before V11L30. + if clidata['version'] < CONSTANTS.QOS_VERSION: + for key, value in qos_specs_dict.items(): + if (key in CONSTANTS.FJ_QOS_KEY_BYTES_list or + key in CONSTANTS.FJ_QOS_KEY_IOPS_list): + msg = (_('_set_qos, Can not support QoS ' + 'parameter "%(key)s" on firmware version ' + '%(version)s.') + % {'key': key, + 'version': clidata['version']}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + if key in CONSTANTS.FJ_QOS_KEY_list: + category_dict = self._get_qos_category_by_value( + key, value) + else: + unsupport.append(key) + if unsupport: + LOG.warning('_set_qos, ' + 'Can not support QoS parameter "%s".', + unsupport) + + # If storage version is after V11L30. + if clidata['version'] >= 'V11L30-0000': + key_dict = self._get_param(qos_specs_dict) + if not key_dict: + return + + # Get total/read/write bandwidth limit. + category_dict = self._get_qos_category(key_dict) + + if category_dict: + # Set volume qos. + volumename = self._get_volume_name(volume, use_id=use_id) + category_dict['volume-name'] = volumename + rc, errordesc, job = self._exec_eternus_cli( + 'set_volume_qos', + **category_dict) + if rc != 0: + msg = (_('_set_qos, ' + 'set_volume_qos failed. ' + 'Return code: %(rc)lu, ' + 'Error: %(errordesc)s, ' + 'Message: %(job)s.') + % {'rc': rc, + 'errordesc': errordesc, + 'job': job}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + @staticmethod + def _get_qos_specs(volume): + """Get qos specs information from volume information.""" + LOG.debug('_get_qos_specs, volume id: %s.', volume['id']) + + qos_specs_dict = {} + qos_specs_id = None + ctxt = None + + volume_type_id = volume.get('volume_type_id') + + if volume_type_id: + ctxt = context.get_admin_context() + volume_type = volume_types.get_volume_type(ctxt, volume_type_id) + qos_specs_id = volume_type.get('qos_specs_id') + + if qos_specs_id: + qos_specs_dict = ( + qos_specs.get_qos_specs(ctxt, qos_specs_id)['specs']) + + LOG.debug('_get_qos_specs, qos_specs_dict: %s.', qos_specs_dict) + return qos_specs_dict + + def _is_qos_or_format_support(self, func_name): + """If storage is DX S2 series, qos or format is not supported.""" + is_support = True + + if self.model_name == CONSTANTS.DX_S2: + is_support = False + LOG.warning('%s is not supported for DX S2, ' + 'Skip this process.', func_name) + return is_support + + @staticmethod + def _get_qos_category_by_value(key, value): + """Get qos category using value.""" + LOG.debug('_get_qos_category_by_value, ' + 'key: %(key)s, value: %(value)s.', + {'key': key, 'value': value}) + + ret = 0 + + # Log error method. + def _get_qos_category_by_value_error(): + """Input value is invalid, log error and raise exception.""" + msg = (_('_get_qos_category_by_value, ' + 'Invalid value is input, ' + 'key: %(key)s, ' + 'value: %(value)s.') + % {'key': key, + 'value': value}) + LOG.warning(msg) + raise exception.VolumeBackendAPIException(data=msg) + + if key == "maxBWS": + try: + digit = int(float(value)) + except Exception: + _get_qos_category_by_value_error() + + if digit >= 800: + ret = 1 + elif digit >= 700: + ret = 2 + elif digit >= 600: + ret = 3 + elif digit >= 500: + ret = 4 + elif digit >= 400: + ret = 5 + elif digit >= 300: + ret = 6 + elif digit >= 200: + ret = 7 + elif digit >= 100: + ret = 8 + elif digit >= 70: + ret = 9 + elif digit >= 40: + ret = 10 + elif digit >= 25: + ret = 11 + elif digit >= 20: + ret = 12 + elif digit >= 15: + ret = 13 + elif digit >= 10: + ret = 14 + elif digit > 0: + ret = 15 + else: + _get_qos_category_by_value_error() + + LOG.debug('_get_qos_category_by_value (%s).', ret) + + category_dict = {} + if ret > 0: + category_dict = {'bandwidth-limit': ret} + + return category_dict + + def _get_param(self, qos_specs_dict): + # Get all keys which have been set and its value. + LOG.debug('_get_param, ' + 'qos_specs_dict: %(qos_specs_dict)s.', + {'qos_specs_dict': qos_specs_dict}) + key_dict = {} + unsupport = [] + for key, value in qos_specs_dict.items(): + if key in CONSTANTS.FJ_QOS_KEY_list: + msg = (_('_get_param, Can not support QoS ' + 'parameter "%(key)s" on firmware version ' + 'V11L30-0000 or above.') + % {'key': key}) + LOG.warning(msg) + raise exception.VolumeBackendAPIException(data=msg) + + if key in CONSTANTS.FJ_QOS_KEY_BYTES_list: + key_dict[key] = self._check_throughput(key, value) + # Example: When "read_bytes_sec" is specified, + # the corresponding "read_iops_sec" also needs to be specified. + # If not, it is specified as the maximum. + iopsStr = key.replace('bytes', 'iops') + if iopsStr not in qos_specs_dict.keys(): + key_dict[iopsStr] = CONSTANTS.MAX_IOPS + elif key in CONSTANTS.FJ_QOS_KEY_IOPS_list: + key_dict[key] = self._check_iops(key, value) + # If can not get the corresponding bytes, + # the bytes is set to the maximum value. + throughputStr = key.replace('iops', 'bytes') + if throughputStr not in qos_specs_dict.keys(): + key_dict[throughputStr] = CONSTANTS.MAX_THROUGHPUT + else: + unsupport.append(key) + if unsupport: + LOG.warning('_get_param, ' + 'Can not support QoS parameter "%s".', unsupport) + + return key_dict + + def _check_iops(self, key, value): + """Check input value of IOPS.""" + LOG.debug('_check_iops, key: %(key)s, value: %(value)s.', + {'key': key, 'value': value}) + value = int(float(value)) + if value < CONSTANTS.MIN_IOPS or value > CONSTANTS.MAX_IOPS: + msg = (_('_check_iops, ' + '%(key)s is out of range.') + % {'key': key}) + LOG.warning(msg) + raise exception.VolumeBackendAPIException(data=msg) + return value + + def _check_throughput(self, key, value): + LOG.debug('_check_throughput, key: %(key)s, value: %(value)s.', + {'key': key, 'value': value}) + value = float(value) / units.Mi + if (value < CONSTANTS.MIN_THROUGHPUT or + value > CONSTANTS.MAX_THROUGHPUT): + msg = (_('_check_throughput, ' + '%(key)s is out of range.') + % {'key': key}) + LOG.warning(msg) + raise exception.VolumeBackendAPIException(data=msg) + return int(value) + + def _get_qos_category(self, key_dict): + """Get qos category by parameters according to the specific volume.""" + LOG.debug('_get_qos_category, ' + 'key_dict: %(key_dict)s.', + {'key_dict': key_dict}) + + # Get all the bandwidth limits. + rc, errordesc, bandwidthlist = self._exec_eternus_cli( + 'show_qos_bandwidth_limit') + if rc != 0: + msg = (_('_get_qos_category, ' + 'show_qos_bandwidth_limit failed. ' + 'Return code: %(rc)lu, ' + 'Error: %(errordesc)s, ' + 'Message: %(clidata)s.') + % {'rc': rc, + 'errordesc': errordesc, + 'clidata': bandwidthlist}) + LOG.warning(msg) + raise exception.VolumeBackendAPIException(data=msg) + + ret_dict = {} + for bw in bandwidthlist: + if 'total_iops_sec' in key_dict.keys(): + if (bw['total_iops_sec'] == key_dict['total_iops_sec'] and + bw['total_bytes_sec'] == key_dict['total_bytes_sec']): + ret_dict['bandwidth-limit'] = bw['total_limit'] + if 'read_iops_sec' in key_dict.keys(): + if (bw['read_iops_sec'] == key_dict['read_iops_sec'] and + bw['read_bytes_sec'] == key_dict['read_bytes_sec']): + ret_dict['read-bandwidth-limit'] = bw['read_limit'] + if 'write_iops_sec' in key_dict.keys(): + if (bw['write_iops_sec'] == key_dict['write_iops_sec'] and + bw['write_bytes_sec'] == key_dict['write_bytes_sec']): + ret_dict['write-bandwidth-limit'] = bw['write_limit'] + + # If find all available pairs. + # len(key_dict) must be 2, 4 or 6 + if len(key_dict) / 2 == len(ret_dict): + return ret_dict + + rc, errordesc, vqosdatalist = self._exec_eternus_cli('show_volume_qos') + if rc != 0: + msg = (_('_get_qos_category, ' + 'show_volume_qos failed. ' + 'Return code: %(rc)lu, ' + 'Error: %(errordesc)s, ' + 'Message: %(clidata)s.') + % {'rc': rc, + 'errordesc': errordesc, + 'clidata': vqosdatalist}) + LOG.warning(msg) + raise exception.VolumeBackendAPIException(data=msg) + + # Get used total/read/write bandwidth limit. + totalusedlimits = set() + readusedlimits = set() + writeusedlimits = set() + for vqos in vqosdatalist: + totalusedlimits.add(vqos['total_limit']) + readusedlimits.add(vqos['read_limit']) + writeusedlimits.add(vqos['write_limit']) + + # Get unused total/read/write bandwidth limit. + totalunusedlimits = list(set(range(1, 16)) - totalusedlimits) + readunusedlimits = list(set(range(1, 16)) - readusedlimits) + writeunusedlimits = list(set(range(1, 16)) - writeusedlimits) + + # If there is no same couple, set new qos bandwidth limit. + if 'total_iops_sec' in key_dict.keys(): + if 'bandwidth-limit' not in ret_dict.keys(): + if len(totalunusedlimits) == 0: + msg = _('_get_qos_category, ' + 'There is no available total bandwidth limit.') + LOG.warning(msg) + raise exception.VolumeBackendAPIException(data=msg) + else: + self._set_limit('volume-qos', + totalunusedlimits[0], + key_dict['total_iops_sec'], + key_dict['total_bytes_sec']) + ret_dict['bandwidth-limit'] = totalunusedlimits[0] + else: + ret_dict['bandwidth-limit'] = 0 + + if 'read_iops_sec' in key_dict.keys(): + if 'read-bandwidth-limit' not in ret_dict.keys(): + if len(readunusedlimits) == 0: + msg = _('_get_qos_category, ' + 'There is no available read bandwidth limit.') + LOG.warning(msg) + raise exception.VolumeBackendAPIException(data=msg) + else: + self._set_limit('volume-qos-read', + readunusedlimits[0], + key_dict['read_iops_sec'], + key_dict['read_bytes_sec']) + ret_dict['read-bandwidth-limit'] = readunusedlimits[0] + else: + ret_dict['read-bandwidth-limit'] = 0 + + if 'write_bytes_sec' in key_dict.keys(): + if 'write-bandwidth-limit' not in ret_dict.keys(): + if len(writeunusedlimits) == 0: + msg = _('_get_qos_category, ' + 'There is no available write bandwidth limit.') + LOG.warning(msg) + raise exception.VolumeBackendAPIException(data=msg) + else: + self._set_limit('volume-qos-write', + writeunusedlimits[0], + key_dict['write_iops_sec'], + key_dict['write_bytes_sec']) + ret_dict['write-bandwidth-limit'] = writeunusedlimits[0] + else: + ret_dict['write-bandwidth-limit'] = 0 + + return ret_dict + + def _set_limit(self, mode, limit, iops, throughput): + """Register a new qos scheme at the specified bandwidth""" + LOG.debug('_set_limit, mode: %(mode)s, ' + 'limit: %(limit)s, iops:%(iops)s, ' + 'throughput: %(throughput)s.', + {'mode': mode, 'limit': limit, + 'iops': iops, 'throughput': throughput}) + param_dict = ({'mode': mode, + 'bandwidth-limit': limit, + 'iops': iops, + 'throughput': throughput}) + + rc, emsg, clidata = self._exec_eternus_cli( + 'set_qos_bandwidth_limit', **param_dict) + + if rc != 0: + msg = (_('_set_limit, ' + 'set_qos_bandwidth_limit failed. ' + 'Return code: %(rc)lu, ' + 'Error: %(errormsg)s, ' + 'Message: %(clidata)s.') + % {'rc': rc, + 'errormsg': emsg, + 'clidata': clidata}) + LOG.warning(msg) + raise exception.VolumeBackendAPIException(data=msg) diff --git a/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_fc.py b/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_fc.py index 550ae38e1da..4aacc9ac152 100644 --- a/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_fc.py +++ b/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_fc.py @@ -20,9 +20,10 @@ FibreChannel Cinder Volume driver for Fujitsu ETERNUS DX S3 series. """ from oslo_log import log as logging -import six from cinder.common import constants +from cinder import exception +from cinder.i18n import _ from cinder import interface from cinder.volume import driver from cinder.volume.drivers.fujitsu.eternus_dx import eternus_dx_common @@ -53,89 +54,50 @@ class FJDXFCDriver(driver.FibreChannelDriver): def check_for_setup_error(self): if not self.common.pywbemAvailable: - LOG.error('pywbem could not be imported! ' - 'pywbem is necessary for this volume driver.') + msg = _('pywbem could not be imported! ' + 'pywbem is necessary for this volume driver.') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) def create_volume(self, volume): """Create volume.""" - LOG.debug('create_volume, ' - 'volume id: %s, enter method.', volume['id']) + model_update = self.common.create_volume(volume) - location, metadata = self.common.create_volume(volume) - - v_metadata = self._get_metadata(volume) - metadata.update(v_metadata) - - LOG.debug('create_volume, info: %s, exit method.', metadata) - return {'provider_location': six.text_type(location), - 'metadata': metadata} + return model_update def create_volume_from_snapshot(self, volume, snapshot): """Creates a volume from a snapshot.""" - LOG.debug('create_volume_from_snapshot, ' - 'volume id: %(vid)s, snap id: %(sid)s, enter method.', - {'vid': volume['id'], 'sid': snapshot['id']}) - location, metadata = ( self.common.create_volume_from_snapshot(volume, snapshot)) v_metadata = self._get_metadata(volume) metadata.update(v_metadata) - LOG.debug('create_volume_from_snapshot, ' - 'info: %s, exit method.', metadata) - return {'provider_location': six.text_type(location), - 'metadata': metadata} + return {'provider_location': str(location), 'metadata': metadata} def create_cloned_volume(self, volume, src_vref): """Create cloned volume.""" - LOG.debug('create_cloned_volume, ' - 'target volume id: %(tid)s, ' - 'source volume id: %(sid)s, enter method.', - {'tid': volume['id'], 'sid': src_vref['id']}) - location, metadata = ( self.common.create_cloned_volume(volume, src_vref)) v_metadata = self._get_metadata(volume) metadata.update(v_metadata) - LOG.debug('create_cloned_volume, ' - 'info: %s, exit method.', metadata) - return {'provider_location': six.text_type(location), - 'metadata': metadata} + return {'provider_location': str(location), 'metadata': metadata} def delete_volume(self, volume): """Delete volume on ETERNUS.""" - LOG.debug('delete_volume, ' - 'volume id: %s, enter method.', volume['id']) - - vol_exist = self.common.delete_volume(volume) - - LOG.debug('delete_volume, ' - 'delete: %s, exit method.', vol_exist) + self.common.delete_volume(volume) def create_snapshot(self, snapshot): """Creates a snapshot.""" - LOG.debug('create_snapshot, ' - 'snap id: %(sid)s, volume id: %(vid)s, enter method.', - {'sid': snapshot['id'], 'vid': snapshot['volume_id']}) - location, metadata = self.common.create_snapshot(snapshot) - LOG.debug('create_snapshot, info: %s, exit method.', metadata) - return {'provider_location': six.text_type(location)} + return {'provider_location': str(location)} def delete_snapshot(self, snapshot): """Deletes a snapshot.""" - LOG.debug('delete_snapshot, ' - 'snap id: %(sid)s, volume id: %(vid)s, enter method.', - {'sid': snapshot['id'], 'vid': snapshot['volume_id']}) - - vol_exist = self.common.delete_snapshot(snapshot) - - LOG.debug('delete_snapshot, ' - 'delete: %s, exit method.', vol_exist) + self.common.delete_snapshot(snapshot) def ensure_export(self, context, volume): """Driver entry point to get the export info for an existing volume.""" @@ -151,10 +113,6 @@ class FJDXFCDriver(driver.FibreChannelDriver): def initialize_connection(self, volume, connector): """Allow connection to connector and return connection info.""" - LOG.debug('initialize_connection, volume id: %(vid)s, ' - 'wwpns: %(wwpns)s, enter method.', - {'vid': volume['id'], 'wwpns': connector['wwpns']}) - info = self.common.initialize_connection(volume, connector) data = info['data'] @@ -163,20 +121,12 @@ class FJDXFCDriver(driver.FibreChannelDriver): data['initiator_target_map'] = init_tgt_map info['data'] = data - LOG.debug('initialize_connection, ' - 'info: %s, exit method.', info) fczm_utils.add_fc_zone(info) return info def terminate_connection(self, volume, connector, **kwargs): """Disallow connection from connector.""" - wwpns = connector.get('wwpns') if connector else None - - LOG.debug('terminate_connection, volume id: %(vid)s, ' - 'wwpns: %(wwpns)s, enter method.', - {'vid': volume['id'], 'wwpns': wwpns}) - - map_exist = self.common.terminate_connection(volume, connector) + self.common.terminate_connection(volume, connector) info = {'driver_volume_type': 'fibre_channel', 'data': {}} @@ -189,15 +139,10 @@ class FJDXFCDriver(driver.FibreChannelDriver): info['data'] = {'initiator_target_map': init_tgt_map} fczm_utils.remove_fc_zone(info) - LOG.debug('terminate_connection, unmap: %(unmap)s, ' - 'connection info: %(info)s, exit method', - {'unmap': map_exist, 'info': info}) return info def get_volume_stats(self, refresh=False): """Get volume stats.""" - LOG.debug('get_volume_stats, refresh: %s, enter method.', refresh) - pool_name = None if refresh is True: data, pool_name = self.common.update_volume_stats() @@ -207,18 +152,12 @@ class FJDXFCDriver(driver.FibreChannelDriver): self._stats = data LOG.debug('get_volume_stats, ' - 'pool name: %s, exit method.', pool_name) + 'pool name: %s.', pool_name) return self._stats def extend_volume(self, volume, new_size): """Extend volume.""" - LOG.debug('extend_volume, ' - 'volume id: %s, enter method.', volume['id']) - - used_pool_name = self.common.extend_volume(volume, new_size) - - LOG.debug('extend_volume, ' - 'used pool name: %s, exit method.', used_pool_name) + self.common.extend_volume(volume, new_size) def _get_metadata(self, volume): v_metadata = volume.get('volume_metadata') diff --git a/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_iscsi.py b/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_iscsi.py index f3d7fdd58d5..8f7df881643 100644 --- a/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_iscsi.py +++ b/cinder/volume/drivers/fujitsu/eternus_dx/eternus_dx_iscsi.py @@ -18,9 +18,10 @@ """iSCSI Cinder Volume driver for Fujitsu ETERNUS DX S3 series.""" from oslo_log import log as logging -import six from cinder.common import constants +from cinder import exception +from cinder.i18n import _ from cinder import interface from cinder.volume import driver from cinder.volume.drivers.fujitsu.eternus_dx import eternus_dx_common @@ -50,35 +51,19 @@ class FJDXISCSIDriver(driver.ISCSIDriver): def check_for_setup_error(self): if not self.common.pywbemAvailable: - LOG.error('pywbem could not be imported! ' - 'pywbem is necessary for this volume driver.') - - return + msg = _('pywbem could not be imported! ' + 'pywbem is necessary for this volume driver.') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) def create_volume(self, volume): """Create volume.""" - LOG.info('create_volume, volume id: %s, Enter method.', volume['id']) + model_update = self.common.create_volume(volume) - element_path, metadata = self.common.create_volume(volume) - - v_metadata = volume.get('volume_metadata') - if v_metadata: - for data in v_metadata: - metadata[data['key']] = data['value'] - else: - v_metadata = volume.get('metadata', {}) - metadata.update(v_metadata) - - LOG.info('create_volume, info: %s, Exit method.', metadata) - return {'provider_location': six.text_type(element_path), - 'metadata': metadata} + return model_update def create_volume_from_snapshot(self, volume, snapshot): """Creates a volume from a snapshot.""" - LOG.info('create_volume_from_snapshot, ' - 'volume id: %(vid)s, snap id: %(sid)s, Enter method.', - {'vid': volume['id'], 'sid': snapshot['id']}) - element_path, metadata = ( self.common.create_volume_from_snapshot(volume, snapshot)) @@ -90,18 +75,10 @@ class FJDXISCSIDriver(driver.ISCSIDriver): v_metadata = volume.get('metadata', {}) metadata.update(v_metadata) - LOG.info('create_volume_from_snapshot, ' - 'info: %s, Exit method.', metadata) - return {'provider_location': six.text_type(element_path), - 'metadata': metadata} + return {'provider_location': str(element_path), 'metadata': metadata} def create_cloned_volume(self, volume, src_vref): """Create cloned volume.""" - LOG.info('create_cloned_volume, ' - 'target volume id: %(tid)s, ' - 'source volume id: %(sid)s, Enter method.', - {'tid': volume['id'], 'sid': src_vref['id']}) - element_path, metadata = ( self.common.create_cloned_volume(volume, src_vref)) @@ -113,40 +90,21 @@ class FJDXISCSIDriver(driver.ISCSIDriver): v_metadata = volume.get('metadata', {}) metadata.update(v_metadata) - LOG.info('create_cloned_volume, info: %s, Exit method.', metadata) - return {'provider_location': six.text_type(element_path), - 'metadata': metadata} + return {'provider_location': str(element_path), 'metadata': metadata} def delete_volume(self, volume): """Delete volume on ETERNUS.""" - LOG.info('delete_volume, volume id: %s, Enter method.', volume['id']) - - vol_exist = self.common.delete_volume(volume) - - LOG.info('delete_volume, delete: %s, Exit method.', vol_exist) - return + self.common.delete_volume(volume) def create_snapshot(self, snapshot): """Creates a snapshot.""" - LOG.info('create_snapshot, snap id: %(sid)s, volume id: %(vid)s, ' - 'Enter method.', - {'sid': snapshot['id'], 'vid': snapshot['volume_id']}) - element_path, metadata = self.common.create_snapshot(snapshot) - LOG.info('create_snapshot, info: %s, Exit method.', metadata) - return {'provider_location': six.text_type(element_path)} + return {'provider_location': str(element_path)} def delete_snapshot(self, snapshot): """Deletes a snapshot.""" - LOG.info('delete_snapshot, snap id: %(sid)s, volume id: %(vid)s, ' - 'Enter method.', - {'sid': snapshot['id'], 'vid': snapshot['volume_id']}) - - vol_exist = self.common.delete_snapshot(snapshot) - - LOG.info('delete_snapshot, delete: %s, Exit method.', vol_exist) - return + self.common.delete_snapshot(snapshot) def ensure_export(self, context, volume): """Driver entry point to get the export info for an existing volume.""" @@ -162,32 +120,16 @@ class FJDXISCSIDriver(driver.ISCSIDriver): def initialize_connection(self, volume, connector): """Allow connection to connector and return connection info.""" - LOG.info('initialize_connection, volume id: %(vid)s, ' - 'initiator: %(initiator)s, Enter method.', - {'vid': volume['id'], 'initiator': connector['initiator']}) - info = self.common.initialize_connection(volume, connector) - LOG.info('initialize_connection, info: %s, Exit method.', info) return info def terminate_connection(self, volume, connector, **kwargs): """Disallow connection from connector.""" - initiator = connector.get('initiator') if connector else None - - LOG.info('terminate_connection, volume id: %(vid)s, ' - 'initiator: %(initiator)s, Enter method.', - {'vid': volume['id'], 'initiator': initiator}) - - map_exist = self.common.terminate_connection(volume, connector) - - LOG.info('terminate_connection, unmap: %s, Exit method.', map_exist) - return + self.common.terminate_connection(volume, connector) def get_volume_stats(self, refresh=False): """Get volume stats.""" - LOG.debug('get_volume_stats, refresh: %s, Enter method.', refresh) - pool_name = None if refresh is True: data, pool_name = self.common.update_volume_stats() @@ -197,14 +139,9 @@ class FJDXISCSIDriver(driver.ISCSIDriver): self._stats = data LOG.debug('get_volume_stats, ' - 'pool name: %s, Exit method.', pool_name) + 'pool name: %s.', pool_name) return self._stats def extend_volume(self, volume, new_size): """Extend volume.""" - LOG.info('extend_volume, volume id: %s, Enter method.', volume['id']) - - used_pool_name = self.common.extend_volume(volume, new_size) - - LOG.info('extend_volume, used pool name: %s, Exit method.', - used_pool_name) + self.common.extend_volume(volume, new_size) diff --git a/doc/source/configuration/block-storage/drivers/fujitsu-eternus-dx-driver.rst b/doc/source/configuration/block-storage/drivers/fujitsu-eternus-dx-driver.rst index 52e6d5ab961..749c95e5b17 100644 --- a/doc/source/configuration/block-storage/drivers/fujitsu-eternus-dx-driver.rst +++ b/doc/source/configuration/block-storage/drivers/fujitsu-eternus-dx-driver.rst @@ -3,7 +3,7 @@ Fujitsu ETERNUS DX driver ========================= Fujitsu ETERNUS DX driver provides FC and iSCSI support for -ETERNUS DX S3 series. +ETERNUS DX series. The driver performs volume operations by communicating with ETERNUS DX. It uses a CIM client in Python called PyWBEM @@ -17,18 +17,23 @@ System requirements Supported storages: -* ETERNUS DX60 S3 -* ETERNUS DX100 S3/DX200 S3 -* ETERNUS DX500 S3/DX600 S3 -* ETERNUS DX8700 S3/DX8900 S3 +* ETERNUS AF150 S3 +* ETERNUS AF250 S3/AF250 S2/AF250 +* ETERNUS AF650 S3/AF650 S2/AF650 * ETERNUS DX200F +* ETERNUS DX60 S5/S4/S3 +* ETERNUS DX100 S5/S4/S3 +* ETERNUS DX200 S5/S4/S3 +* ETERNUS DX500 S5/S4/S3 +* ETERNUS DX600 S5/S4/S3 +* ETERNUS DX8700 S3/DX8900 S4/S3 Requirements: * Firmware version V10L30 or later is required. * The multipath environment with ETERNUS Multipath Driver is unsupported. * An Advanced Copy Feature license is required - to create a snapshot and a clone. + to create snapshots, create volume from snapshots, or clone volumes. Supported operations ~~~~~~~~~~~~~~~~~~~~ @@ -60,9 +65,10 @@ Perform the following steps using ETERNUS Web GUI or ETERNUS CLI. .. note:: * These following operations require an account that has the ``Admin`` role. * For detailed operations, refer to ETERNUS Web GUI User's Guide or - ETERNUS CLI User's Guide for ETERNUS DX S3 series. + ETERNUS CLI User's Guide for ETERNUS DX series. -#. Create an account for communication with cinder controller. +#. Create an account with software role for communication + with cinder controller. #. Enable the SMI-S of ETERNUS DX. @@ -77,17 +83,21 @@ Perform the following steps using ETERNUS Web GUI or ETERNUS CLI. #. Create Snap Data Pool Volume (SDPV) to enable Snap Data Pool (SDP) for ``create a snapshot``. -#. Configure storage ports used for OpenStack. +#. Configure storage ports to be used by the Block Storage service. - - Set those storage ports to CA mode. - - Enable the host-affinity settings of those storage ports. + * Set those storage ports to CA mode. + * Enable the host-affinity settings of those storage ports. (ETERNUS CLI command for enabling host-affinity settings): .. code-block:: console - CLI> set fc-parameters -host-affinity enable -port - CLI> set iscsi-parameters -host-affinity enable -port + CLI> set fc-parameters -host-affinity enable -port + CLI> set iscsi-parameters -host-affinity enable -port + + .. note:: + * Replace and with the name of the controller enclosure where the port is located. + * Replace with the port number. #. Ensure LAN connection between cinder controller and MNT port of ETERNUS DX and SAN connection between Compute nodes and CA ports of ETERNUS DX. @@ -160,28 +170,30 @@ Configuration Where: ``EternusIP`` - IP address for the SMI-S connection of the ETRENUS DX. + IP address of the SMI-S connection of the ETRENUS device. - Enter the IP address of MNT port of the ETERNUS DX. + Use the IP address of the MNT port of device. ``EternusPort`` - Port number for the SMI-S connection port of the ETERNUS DX. + Port number for the SMI-S connection port of the ETERNUS device. ``EternusUser`` - User name for the SMI-S connection of the ETERNUS DX. + User name of ``sofware`` role for the connection ``EternusIP``. ``EternusPassword`` - Password for the SMI-S connection of the ETERNUS DX. + Corresponding password of ``EternusUser`` on ``EternusIP``. ``EternusPool`` (Multiple setting allowed) - Storage pool name for volumes. + Name of the storage pool for the volumes from ``ETERNUS DX setup``. - Enter RAID Group name or TPP name in the ETERNUS DX. + Use the pool RAID Group name or TPP name in the ETERNUS device. ``EternusSnapPool`` - Storage pool name for snapshots. + Name of the storage pool for the snapshots from ``ETERNUS DX setup``. - Enter RAID Group name in the ETERNUS DX. + Use the pool RAID Group name in the ETERNUS device. + + If you did not create a different pool for snapshots, use the same value as ``ETternusPool``. ``EternusISCSIIP`` (Multiple setting allowed) iSCSI connection IP address of the ETERNUS DX. @@ -221,11 +233,166 @@ Configuration example .. code-block:: console - $ openstack volume type create DX_FC - $ openstack volume type set --property volume_backend_name=FC DX_FX - $ openstack volume type create DX_ISCSI - $ openstack volume type set --property volume_backend_name=ISCSI DX_ISCSI + $ cinder type-create DX_FC + $ cinder type-key DX_FX set volume_backend_name=FC + $ cinder type-create DX_ISCSI + $ cinder type-key DX_ISCSI set volume_backend_name=ISCSI By issuing these commands, the volume type ``DX_FC`` is associated with the ``FC``, and the type ``DX_ISCSI`` is associated with the ``ISCSI``. + + +Supplementary Information for the Supported Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +QoS Settings +------------ + +The QoS settings that are linked with the volume QoS function of the +ETERNUS AF/DX are available. + +An upper limit value of the bandwidth(BWS) can be set for each volume. +A lower limit value can not be set. + +The upper limit is set if the firmware version of the ETERNUS AF/DX is +earlier than V11L30, and the IOPS/Throughput of +Total/Read/Write for the volume is set separately for V11L30 and later. + +The following procedure shows how to set the QoS. + +#. Create a QoS definition. + + * The firmware version of the ETERNUS AF/DX is earlier than V11L30 + + .. code-block:: ini + + $ cinder qos-create maxBWS=xx + + For , specify the name of the definition that is to be created. + + For maxBWS, specify a value in MB. + + * The firmware version of the ETERNUS AF/DX is V11L30 or later + + .. code-block:: console + + $ cinder qos-create read_iops_sec=15000 write_iops_sec=12600 total_iops_sec=15000 read_bytes_sec=800 write_bytes_sec=700 total_bytes_sec=800 + +#. When not using the existing volume type, create a new volume type. + + .. code-block:: console + + $ cinder type-create + + For , specify the name of the volume type that is to be created. + +#. Associate the QoS definition with the volume type. + + .. code-block:: console + + $ cinder qos-associate + + For , specify the ID of the QoS definition that was created. + + For , specify the ID of the volume type that was created. + +**Cautions** + +#. For the procedure to cancel the QoS settings, + refer to "OpenStack Command-Line Interface Reference". + +#. The QoS mode of the ETERNUS AF/DX must be enabled in advance. + For details, refer to the ETERNUS Web GUI manuals. + +#. When the firmware version of the ETERNUS AF/DX is earlier than V11L30, + for the volume QoS settings of the ETERNUS AF/DX, upper limits are set + using the predefined options. + + Therefore, set the upper limit of the ETERNUS AF/DX side to a maximum value + that does not exceed the specified maxBWS. + + The following table shows the upper limits that can be set on the + ETERNUS AF/DX side and example settings. + For details about the volume QoS settings of the ETERNUS AF/DX, + refer to the ETERNUS Web GUI manuals. + + +--------------------------------+ + | Settings for the ETERNUS AF/DX | + +================================+ + | Unlimited | + +--------------------------------+ + | 15000 IOPS (800MB/s) | + +--------------------------------+ + | 12600 IOPS (700MB/s) | + +--------------------------------+ + | 10020 IOPS (600MB/s) | + +--------------------------------+ + | 7500 IOPS (500MB/s) | + +--------------------------------+ + | 5040 IOPS (400MB/s) | + +--------------------------------+ + | 3000 IOPS (300MB/s) | + +--------------------------------+ + | 1020 IOPS (200MB/s) | + +--------------------------------+ + | 780 IOPS (100MB/s) | + +--------------------------------+ + | 600 IOPS (70MB/s) | + +--------------------------------+ + | 420 IOPS (40MB/s) | + +--------------------------------+ + | 300 IOPS (25MB/s) | + +--------------------------------+ + | 240 IOPS (20MB/s) | + +--------------------------------+ + | 180 IOPS (15MB/s) | + +--------------------------------+ + | 120 IOPS (10MB/s) | + +--------------------------------+ + | 60 IOPS (5MB/s) | + +--------------------------------+ + + * When specified maxBWS=750 + + "12600 IOPS (700MB/s)" is set on the ETERNUS AF/DX side. + + * When specified maxBWS=900 + + "15000 IOPS (800MB/s)" is set on the ETERNUS AF/DX side. + +#. While a QoS definition is being created, if an option other than + maxBWS/read_iops_sec/write_iops_sec/total_iops_sec/read_bytes_sec + /write_bytes_sec/total_bytes_sec is specified, + a warning log is output and the QoS information setting is continued. + +#. For an ETERNUS AF/DX wth a firmware version of before V11L30, + if a QoS definition volume type that is set with read_iops_sec/ + write_iops_sec/total_iops_sec/read_bytes_sec/write_bytes_sec/total_bytes_sec + is specified for Create Volume, a warning log is output + and the process is terminated. + +#. For an ETERNUS AF/DX with a firmware version of V11L30 or later, + if a QoS definition volume type that is set with maxBWS is specified + for Create Volume, a warning log is output and the process is terminated. + +#. After the firmware of the ETERNUS AF/DX is upgraded from V11L10/V11L2x to + a newer version, the volume types related to the QoS definition created + before the firmware upgrade can no longer be used. + Set a QoS definition and create a new volume type. + +#. When the firmware of the ETERNUS AF/DX is downgraded to V11L10/V11L2x, + do not use a volume type linked to a pre-firmware downgrade + QoS definition, because the QoS definition may work differently from + ones post-firmware downgrade. + For the volume, create and link a volume type not associated with + any QoS definition and after the downgrade, create and link a volume type + associated with a QoS definition. + +#. If Create Volume terminates with an error, Cinder may not invoke + Delete Volume. + + If volumes are created but the QoS settings fail, the + ETERNUS OpenStack VolumeDriver ends the process to prevent the + created volumes from being left in the ETERNUS AF/DX. + If volumes fail to be created, the process terminates with an error. diff --git a/releasenotes/notes/fujitsu-qos-support-1c1528da06d0b38a.yaml b/releasenotes/notes/fujitsu-qos-support-1c1528da06d0b38a.yaml new file mode 100644 index 00000000000..4c88b88d61e --- /dev/null +++ b/releasenotes/notes/fujitsu-qos-support-1c1528da06d0b38a.yaml @@ -0,0 +1,33 @@ +--- +features: + - | + Fujitsu ETERNUS DX driver: Added support for QoS + + What QoS settings are available depends upon the storage firmware version + of the ETERNUS AF/DX. + + * When the storage firmware version is less than V11L30-0000, + only the upper limit of bandwidth(BWS) can be set using: + + - ``maxBWS`` + + Note that when the firmware version of the ETERNUS AF/DX is earlier + than V11L30, upper limits for the volume QoS settings of + the ETERNUS AF/DX are set using predefined options. This means that + you should set the upper limit *of the ETERNUS AF/DX side* to a maximum + value that does not exceed the specified ``maxBWS``. + + * When the storage firmware version is greater than V11L30-0000, + the IOPS/Throughput of Total/Read/Write for the volume can be set + separately using: + + - ``read_bytes_sec`` + - ``write_bytes_sec`` + - ``total_bytes_sec`` + - ``read_iops_sec`` + - ``write_iops_sec`` + - ``total_iops_sec`` + + See the `Fujitsu ETERNUS DX driver documentation + `_ + for details.