Fujitsu Driver: Add QoS support

Added support for QoS in the Fujitsu driver.

The supported qos specs are:

* maxBWS
* read_bytes_sec
* write_bytes_sec
* total_bytes_sec
* read_iops_sec
* write_iops_sec
* total_iops_sec

Change-Id: I0f12c90f9483393501cf5788b66f724d47e72c8e
This commit is contained in:
Xu Qi 2022-06-27 01:31:38 -04:00 committed by inori
parent f79048d282
commit e954ba02de
8 changed files with 1815 additions and 368 deletions

View File

@ -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)

View File

@ -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',

View File

@ -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)

File diff suppressed because it is too large Load Diff

View File

@ -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')

View File

@ -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)

View File

@ -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 <CM#><CA#><Port#>
CLI> set iscsi-parameters -host-affinity enable -port <CM#><CA#><Port#>
CLI> set fc-parameters -host-affinity enable -port <CM#><CA#><Port>
CLI> set iscsi-parameters -host-affinity enable -port <CM#><CA#><Port>
.. note::
* Replace <CM#> and <CA#> with the name of the controller enclosure where the port is located.
* Replace <Port> 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 <qos_name> maxBWS=xx
For <qos_name>, 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 <qos_name> 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 <volume_type_name>
For <volume_type_name>, 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 <qos_specs> <volume_type_id>
For <qos_specs>, specify the ID of the QoS definition that was created.
For <volume_type_id>, 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.

View File

@ -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
<https://docs.openstack.org/cinder/latest/configuration/block-storage/drivers/fujitsu-eternus-dx-driver.html>`_
for details.