Merge "Fujitsu Driver: Improve CLI function"

This commit is contained in:
Zuul 2025-02-18 14:17:38 +00:00 committed by Gerrit Code Review
commit debc28490f
6 changed files with 2136 additions and 179 deletions

View File

@ -24,6 +24,7 @@ from cinder import exception
from cinder import ssh_utils
from cinder.tests.unit import test
from cinder.volume import configuration as conf
from cinder.volume.drivers.fujitsu.eternus_dx import constants as CONSTANTS
with mock.patch.dict('sys.modules', pywbem=mock.Mock()):
from cinder.volume.drivers.fujitsu.eternus_dx \
@ -35,6 +36,8 @@ with mock.patch.dict('sys.modules', pywbem=mock.Mock()):
from cinder.volume.drivers.fujitsu.eternus_dx \
import eternus_dx_iscsi as dx_iscsi
PRIVATE_KEY_PATH = '/etc/cinder/eternus'
CONFIG_FILE_NAME = 'cinder_fujitsu_eternus_dx.xml'
STORAGE_SYSTEM = '172.16.0.2'
@ -111,7 +114,7 @@ TEST_CONNECTOR = {'initiator': ISCSI_INITIATOR, 'wwpns': TEST_WWPN}
STORAGE_IP = '172.16.0.2'
TEST_USER = 'testuser'
TEST_PASSWORD = 'testpassword'
TEST_PASSWORD = 'testpass'
STOR_CONF_SVC = 'FUJITSU_StorageConfigurationService'
CTRL_CONF_SVC = 'FUJITSU_ControllerConfigurationService'
@ -181,7 +184,7 @@ FAKE_POOLS = [{
}]
FAKE_STATS = {
'driver_version': '1.4.7',
'driver_version': '1.4.8',
'storage_protocol': 'iSCSI',
'vendor_name': 'FUJITSU',
'QoS_support': True,
@ -191,7 +194,7 @@ FAKE_STATS = {
'pools': FAKE_POOLS,
}
FAKE_STATS2 = {
'driver_version': '1.4.7',
'driver_version': '1.4.8',
'storage_protocol': 'FC',
'vendor_name': 'FUJITSU',
'QoS_support': True,
@ -353,7 +356,7 @@ FAKE_MODEL_INFO2 = {
FAKE_CLI_OUTPUT = {
"result": 0,
'rc': '0',
'rc': str(CONSTANTS.RC_OK),
"message": 'TEST_MESSAGE'
}
@ -429,7 +432,7 @@ class FakeEternusConnection(object):
global MAP_STAT, VOL_STAT
if MethodName == 'CreateOrModifyElementFromStoragePool':
VOL_STAT = '1'
rc = 0
rc = CONSTANTS.RC_OK
vol = self._enum_volumes()
if InPool.get('InstanceID') == 'FUJITSU:RSP0005':
job = {'TheElement': vol[1].path}
@ -440,29 +443,29 @@ class FakeEternusConnection(object):
job = {'TheElement': vol[0].path}
elif MethodName == 'ReturnToStoragePool':
VOL_STAT = '0'
rc = 0
rc = CONSTANTS.RC_OK
job = {}
elif MethodName == 'GetReplicationRelationships':
rc = 0
rc = CONSTANTS.RC_OK
job = {'Synchronizations': []}
elif MethodName == 'ExposePaths':
MAP_STAT = '1'
rc = 0
rc = CONSTANTS.RC_OK
job = {}
elif MethodName == 'HidePaths':
MAP_STAT = '0'
rc = 0
rc = CONSTANTS.RC_OK
job = {}
elif MethodName == 'CreateElementReplica':
rc = 0
rc = CONSTANTS.RC_OK
snap = self._enum_snapshots()
job = {'TargetElement': snap[0].path}
elif MethodName == 'CreateReplica':
rc = 0
rc = CONSTANTS.RC_OK
snap = self._enum_snapshots()
job = {'TargetElement': snap[0].path}
elif MethodName == 'ModifyReplicaSynchronization':
rc = 0
rc = CONSTANTS.RC_OK
job = {}
else:
raise exception.VolumeBackendAPIException(data="invoke method")
@ -1083,6 +1086,8 @@ class FJFCDriverTestCase(test.TestCase):
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.configuration.fujitsu_passwordless = False
self.configuration.fujitsu_private_key_path = PRIVATE_KEY_PATH
self.configuration.fujitsu_use_cli_copy = False
self.mock_object(dx_common.FJDXCommon, '_get_eternus_connection',
@ -1398,6 +1403,8 @@ class FJISCSIDriverTestCase(test.TestCase):
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.configuration.fujitsu_passwordless = False
self.configuration.fujitsu_private_key_path = PRIVATE_KEY_PATH
self.configuration.fujitsu_use_cli_copy = False
self.mock_object(dx_common.FJDXCommon, '_get_eternus_connection',
@ -1781,31 +1788,28 @@ class FJCLITestCase(test.TestCase):
ret = '%s\r\n00\r\n0019\r\nCLI> ' % exec_cmdline
elif exec_cmdline.startswith('start copy-opc'):
ret = '%s\r\n00\r\n0019\r\nCLI> ' % exec_cmdline
elif exec_cmdline.startswith('show cli-error-code'):
ret = '%s\r\n00\r\n0001\r\n0001\tBad Value\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}
def test_show_cli_error_message(self):
FAKE_OPTION = {'error-code': '0001'}
FAKE_MESSAGE = 'Bad Value'
FAKE_MESSAGE_OUTPUT = {**FAKE_CLI_OUTPUT, 'message': FAKE_MESSAGE}
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_MESSAGE_OUTPUT = self.cli._show_cli_error_message(**FAKE_OPTION)
self.assertEqual(FAKE_MESSAGE_OUTPUT, ERROR_MESSAGE_OUTPUT)
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
def test_create_error_message(self):
FAKE_CODE = '0001'
FAKE_MSG = 'Bad Value'
expected_error_message = ('E' + FAKE_CODE, FAKE_MSG)
error_qos_output = self.cli._set_volume_qos(**FAKE_QOS_OPTION)
self.assertEqual(expected_error_value, error_qos_output)
ERROR_MESSAGE = self.cli._create_error_message(FAKE_CODE,
FAKE_MSG)
self.assertEqual(expected_error_message, ERROR_MESSAGE)
def test_get_options(self):
expected_option = " -bandwidth-limit 2"
@ -2005,6 +2009,8 @@ class FJCommonTestCase(test.TestCase):
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.configuration.fujitsu_passwordless = False
self.configuration.fujitsu_private_key_path = PRIVATE_KEY_PATH
self.configuration.fujitsu_use_cli_copy = False
self.mock_object(dx_common.FJDXCommon, '_get_eternus_connection',
@ -2107,6 +2113,101 @@ class FJCommonTestCase(test.TestCase):
for key, value in diction.items():
volume[key] = value
@mock.patch.object(ssh_utils, 'SSHPool')
def test_ssh_to_storage_by_password(self, mock_ssh_pool):
command = 'show_enclosure_status'
self.driver.common.fjdxcli = {}
self.driver.common._exec_eternus_cli(command)
mock_ssh_pool.assert_called_with(STORAGE_IP, 22, None, TEST_USER,
password=TEST_PASSWORD, max_size=2)
@mock.patch.object(ssh_utils, 'SSHPool')
def test_ssh_to_storage_by_key(self, mock_ssh_pool):
command = 'show_enclosure_status'
self.configuration.fujitsu_passwordless = True
driver = dx_iscsi.FJDXISCSIDriver(configuration=self.configuration)
self.driver = driver
self.driver.common.fjdxcli = {}
self.driver.common._exec_eternus_cli(command)
mock_ssh_pool.assert_called_with(STORAGE_IP, 22, None, TEST_USER,
privatekey=PRIVATE_KEY_PATH,
max_size=2)
def test_exec_eternus_cli_success(self):
command = 'show_enclosure_status'
FAKE_CLI_ENCLOUSER_STATUS = (0, None, {'version': 'V10L87-9000'})
cli_enclosure_status = self.driver.common._exec_eternus_cli(command)
self.assertEqual(FAKE_CLI_ENCLOUSER_STATUS, cli_enclosure_status)
@mock.patch.object(eternus_dx_cli.FJDXCLI, '_exec_cli_with_eternus')
def test_exec_eternus_cli_success_with_retry(self,
mock_exec_cli_with_eternus):
command = 'stop_copy_session'
mock_exec_cli_with_eternus.side_effect = [
'\r\nCLI> stop copy-session\r\n01\r\n0060\r\nCLI> ',
'\r\nCLI> show cli-error-code -error-code '
'0060\r\n00\r\n0060\r\n0060\tResource locked\r\nCLI> ',
'\r\nCLI> stop copy-session\r\n00\r\nCLI> ']
retry_msg = 'INFO:cinder.volume.drivers.fujitsu.eternus_dx.' \
'eternus_dx_common:_exec_eternus_cli, retry, ' \
'ip: 172.16.0.2, RetryCode: E0060, TryNum: 1.'
FAKE_STOP_COPY_SESSION = (0, None, [])
with self.assertLogs('cinder.volume.drivers.fujitsu.eternus_dx.'
'eternus_dx_common', level='INFO') as cm:
cli_return = self.driver.common._exec_eternus_cli(command)
self.assertIn(retry_msg, cm.output)
self.assertEqual(FAKE_STOP_COPY_SESSION, cli_return)
@mock.patch.object(eternus_dx_cli.FJDXCLI, '_exec_cli_with_eternus')
def test_exec_eternus_cli_authentication_fail(self,
mock_exec_cli_with_eternus):
command = 'check_user_role'
mock_exec_cli_with_eternus.side_effect = (
exception.VolumeBackendAPIException(
'Execute CLI command error. Error: Authentication failed.'))
authentication_fail_msg = 'WARNING:' \
'cinder.volume.drivers.fujitsu.eternus_dx.' \
'eternus_dx_common:_exec_eternus_cli, ' \
'retry, ip: 172.16.0.2, ' \
'Message: Execute CLI command error. ' \
'Error: Authentication failed., TryNum: 1.'
FAKE_STOP_COPY_SESSION = (4, '4',
'Execute CLI command error. '
'Error: Authentication failed.')
with self.assertLogs('cinder.volume.drivers.fujitsu.eternus_dx.'
'eternus_dx_common', level='WARNING') as cm:
cli_return = self.driver.common._exec_eternus_cli(command)
self.assertIn(authentication_fail_msg, cm.output)
self.assertEqual(FAKE_STOP_COPY_SESSION, cli_return)
@mock.patch.object(eternus_dx_cli.FJDXCLI, '_exec_cli_with_eternus')
@mock.patch.object(dx_common, 'LOG')
def test_exec_eternus_cli_retry_exceed(self, mock_log,
mock_exec_cli_with_eternus):
command = 'stop_copy_session'
mock_exec_cli_with_eternus.side_effect = [
'\r\nCLI> stop copy-session\r\n01\r\n0060\r\nCLI> ',
'\r\nCLI> show cli-error-code -error-code '
'0060\r\n00\r\n0060\r\n0060\tResource locked\r\nCLI> '] * 3
exceed_msg = '_exec_eternus_cli, Retry was exceeded.'
FAKE_STOP_COPY_SESSION = (4, 'E0060', 'Resource locked')
cli_return = self.driver.common._exec_eternus_cli(command)
mock_log.warning.assert_called_with(exceed_msg)
self.assertEqual(FAKE_STOP_COPY_SESSION, cli_return)
def test_get_eternus_model(self):
ETERNUS_MODEL = self.driver.common._get_eternus_model()
self.assertEqual(3, ETERNUS_MODEL)

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@
from cinder.i18n import _
from cinder import ssh_utils
from cinder.volume.drivers.fujitsu.eternus_dx import constants as CONSTANTS
class FJDXCLI(object):
@ -59,12 +60,6 @@ class FJDXCLI(object):
'delete_volume': self._delete_volume
}
self.SMIS_dic = {
'0000': '0', # Success.
'0060': '32787', # The device is in busy state.
'0100': '4097'
} # Size not supported.
def done(self, command, **option):
func = self.CMD_dic.get(command, self._default_func)
return func(**option)
@ -105,7 +100,7 @@ class FJDXCLI(object):
'stdoutlist': stdoutlist})
if status == 0:
rc = '0'
rc = str(CONSTANTS.RC_OK)
for outline in stdoutlist[lineno:]:
if 0 <= outline.find('CLI>'):
continue
@ -124,8 +119,11 @@ class FJDXCLI(object):
if outline is None:
continue
output.append(outline)
rc, message = self._create_error_message(code, output)
if cmd != "show cli-error-code":
rc, message = self._create_error_message(code, output)
else:
rc = 'E' + code
message = output
return {'result': 0, 'rc': rc, 'message': message}
@ -160,22 +158,29 @@ class FJDXCLI(object):
self.ssh_pool.remove(ssh)
return stdoutdata
def _show_cli_error_message(self, **option):
"""Get error messages by error code."""
output = self._exec_cli("show cli-error-code",
**option)
rc = output['rc']
if rc != str(CONSTANTS.RC_OK):
raise Exception(_('_show_cli_error_message failed. '
'Return code: %lu') % rc)
message = output['message'][1]
output['message'] = message.split('\t')[1]
return output
def _create_error_message(self, code, msg):
"""Create error code and message using arguements."""
message = None
if code in self.SMIS_dic:
rc = self.SMIS_dic[code]
else:
rc = 'E' + code
# TODO(whfnst): we will have a dic to store errors.
if rc == "E0001":
message = "Bad value: %s" % msg
elif rc == "ED184":
message = "Because OPC is being executed, "
"the processing was discontinued."
else:
message = msg
rc = 'E' + code
try:
option = {
'error-code': code
}
message = self._show_cli_error_message(**option)['message']
except Exception:
message = CONSTANTS.CLIRETCODE_dic.get(rc, msg)
return rc, message
@ -214,7 +219,7 @@ class FJDXCLI(object):
**option)
# Return error.
rc = output['rc']
if rc != "0":
if rc != str(CONSTANTS.RC_OK):
return output
userlist = output.get('message')
@ -237,7 +242,7 @@ class FJDXCLI(object):
msg = str(ex)
output = {
'result': 0,
'rc': '4',
'rc': str(CONSTANTS.RC_FAILED),
'message': msg
}
return output
@ -257,7 +262,7 @@ class FJDXCLI(object):
rc = output['rc']
if rc != "0":
if rc != str(CONSTANTS.RC_OK):
return output
clidatalist = output.get('message')
@ -274,7 +279,7 @@ class FJDXCLI(object):
except Exception as ex:
output = {
'result': 0,
'rc': '4',
'rc': str(CONSTANTS.RC_FAILED),
'message': "show pool provision capacity error: %s" % ex
}
@ -288,7 +293,7 @@ class FJDXCLI(object):
# return error
rc = output['rc']
if rc != "0":
if rc != str(CONSTANTS.RC_OK):
return output
cpsdatalist = []
@ -360,7 +365,7 @@ class FJDXCLI(object):
output['message'] = cpsdatalist
except Exception as ex:
output = {'result': 0,
'rc': '4',
'rc': str(CONSTANTS.RC_FAILED),
'message': "Show copy sessions error: %s"
% str(ex)}
@ -375,7 +380,7 @@ class FJDXCLI(object):
# return error
rc = output['rc']
if rc != "0":
if rc != str(CONSTANTS.RC_OK):
return output
qoslist = []
@ -399,13 +404,13 @@ class FJDXCLI(object):
msg = ('The results returned by cli are not as expected. '
'Exception string: %s' % clidata)
output = {'result': 0,
'rc': '4',
'rc': str(CONSTANTS.RC_FAILED),
'message': "Show qos bandwidth limit error: %s. %s"
% (ex, msg)}
except Exception as ex:
output = {'result': 0,
'rc': '4',
'rc': str(CONSTANTS.RC_FAILED),
'message': "Show qos bandwidth limit error: %s" % ex}
return output
@ -423,7 +428,7 @@ class FJDXCLI(object):
# return error
rc = output['rc']
if rc != "0":
if rc != str(CONSTANTS.RC_OK):
return output
vqosdatalist = []
@ -441,12 +446,12 @@ class FJDXCLI(object):
msg = ('The results returned by cli are not as expected. '
'Exception string: %s' % clidata)
output = {'result': 0,
'rc': '4',
'rc': str(CONSTANTS.RC_FAILED),
'message': "Show volume qos error: %s. %s" % (ex, msg)}
except Exception as ex:
output = {'result': 0,
'rc': '4',
'rc': str(CONSTANTS.RC_FAILED),
'message': "Show volume qos error: %s" % ex}
return output
@ -460,7 +465,7 @@ class FJDXCLI(object):
# return error
rc = output['rc']
if rc != "0":
if rc != str(CONSTANTS.RC_OK):
return output
clidatalist = output.get('message')
@ -473,13 +478,13 @@ class FJDXCLI(object):
msg = ('The results returned by cli are not as expected. '
'Exception string: %s' % clidata)
output = {'result': 0,
'rc': '4',
'rc': str(CONSTANTS.RC_FAILED),
'message': "Show enclosure status error: %s. %s"
% (ex, msg)}
except Exception as ex:
output = {'result': 0,
'rc': '4',
'rc': str(CONSTANTS.RC_FAILED),
'message': "Show enclosure status error: %s" % ex}
return output

View File

@ -53,6 +53,14 @@ FJ_ETERNUS_DX_OPT_opts = [
cfg.StrOpt('cinder_eternus_config_file',
default='/etc/cinder/cinder_fujitsu_eternus_dx.xml',
help='Config file for cinder eternus_dx volume driver.'),
cfg.BoolOpt('fujitsu_passwordless',
default=True,
help='Use SSH key to connect to storage.'),
cfg.StrOpt('fujitsu_private_key_path',
default='$state_path/eternus',
help='Filename of private key for ETERNUS CLI. '
'This option must be set when '
'the fujitsu_passwordless is True.'),
cfg.BoolOpt('fujitsu_use_cli_copy',
default=False,
help='If True use CLI command to create snapshot.'),
@ -76,10 +84,12 @@ class FJDXCommon(object):
1.4.5 - Add metadata for snapshot.
1.4.6 - Add parameter fujitsu_use_cli_copy.
1.4.7 - Add support for revert-to-snapshot.
1.4.8 - Improve the processing flow of CLI error messages.(bug #2048850)
- Add support connect to storage using SSH key.
"""
VERSION = "1.4.7"
VERSION = "1.4.8"
stats = {
'driver_version': VERSION,
'storage_protocol': None,
@ -97,6 +107,8 @@ class FJDXCommon(object):
self.configuration.append_config_values(FJ_ETERNUS_DX_OPT_opts)
self.conn = None
self.passwordless = self.configuration.fujitsu_passwordless
self.private_key_path = self.configuration.fujitsu_private_key_path
self.use_cli_copy = self.configuration.fujitsu_use_cli_copy
self.fjdxcli = {}
self.model_name = self._get_eternus_model()
@ -222,7 +234,7 @@ class FJDXCommon(object):
'Element Name is in use.',
{'volumename': volumename})
element = self._find_lun(volume)
elif rc != 0:
elif rc != CONSTANTS.RC_OK:
msg = (_('_create_volume, '
'volumename: %(volumename)s, '
'poolname: %(eternus_pool)s, '
@ -440,7 +452,7 @@ class FJDXCommon(object):
SourceElement=src_vol_instance.path,
TargetElement=tgt_vol_instance.path)
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('_create_local_cloned_volume, '
'volumename: %(volumename)s, '
'sourcevolumename: %(sourcevolumename)s, '
@ -546,7 +558,7 @@ class FJDXCommon(object):
'stop_copy_session',
**param_dict)
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('_delete_volume_setting, '
'stop_copy_session failed. '
'Return code: %(rc)lu, '
@ -601,7 +613,7 @@ class FJDXCommon(object):
configservice,
TheElement=vol_instance.path)
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('_delete_volume, volumename: %(volumename)s, '
'Return code: %(rc)lu, '
'Error: %(errordesc)s.')
@ -630,7 +642,7 @@ class FJDXCommon(object):
'delete_volume',
**param_dict)
if rc == 0:
if rc == CONSTANTS.RC_OK:
msg = (_('_delete_volume_after_error, '
'volumename: %(volumename)s, '
'Delete Successed.')
@ -774,7 +786,7 @@ class FJDXCommon(object):
smis_method, smis_service,
**params)
if rc != 0:
if rc != CONSTANTS.RC_OK:
LOG.warning('_create_snapshot, '
'snapshotname: %(snapshotname)s, '
'source volume name: %(volumename)s, '
@ -826,14 +838,14 @@ class FJDXCommon(object):
ElementType=self._pywbem_uint(pooltype, '16'),
Size=self._pywbem_uint(vol_size, '64'))
if rc == 32769:
if rc == CONSTANTS.RG_VOLNUM_MAX:
LOG.warning('_create_snapshot, RAID Group pool: %s. '
'Maximum number of Logical Volume in a '
'RAID Group has been reached. '
'Try other pool.',
pool)
continue
elif rc != 0:
elif rc != CONSTANTS.RC_OK:
msg = (_('_create_volume, '
'volumename: %(volumename)s, '
'poolname: %(eternus_pool)s, '
@ -862,7 +874,7 @@ class FJDXCommon(object):
'start_copy_snap_opc',
**param_dict)
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('_create_snapshot, '
'create_volume failed. '
'Return code: %(rc)lu, '
@ -1079,7 +1091,7 @@ class FJDXCommon(object):
'expand_volume',
**param_dict)
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('extend_volume, '
'volumename: %(volumename)s, '
'Return code: %(rc)lu, '
@ -1133,7 +1145,7 @@ class FJDXCommon(object):
Size=self._pywbem_uint(volumesize, '64'),
TheElement=volume_instance.path)
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('extend_volume, '
'volumename: %(volumename)s, '
'Return code: %(rc)lu, '
@ -1543,7 +1555,7 @@ class FJDXCommon(object):
'show_pool_provision',
**param_dict)
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('_find_pools, show_pool_provision, '
'pool name: %(pool_name)s, '
'Return code: %(rc)lu, '
@ -2047,7 +2059,7 @@ class FJDXCommon(object):
% {'cpsession': cpsession,
'operation': operation})
raise exception.VolumeIsBusy(msg)
elif rc != 0:
elif rc != CONSTANTS.RC_OK:
msg = (_('_delete_copysession, '
'copysession: %(cpsession)s, '
'operation: %(operation)s, '
@ -2188,7 +2200,7 @@ class FJDXCommon(object):
{'errordesc': errordesc,
'rc': rc})
if rc != 0 and rc != CONSTANTS.LUNAME_IN_USE:
if rc != CONSTANTS.RC_OK and rc != CONSTANTS.LUNAME_IN_USE:
LOG.warning('_map_lun, '
'lun_name: %(volume_uid)s, '
'Initiator: %(initiator)s, '
@ -2221,7 +2233,7 @@ class FJDXCommon(object):
{'errordesc': errordesc,
'rc': rc})
if rc != 0 and rc != CONSTANTS.LUNAME_IN_USE:
if rc != CONSTANTS.RC_OK and rc != CONSTANTS.LUNAME_IN_USE:
LOG.warning('_map_lun, '
'lun_name: %(volume_uid)s, '
'Initiator: %(initiator)s, '
@ -2428,7 +2440,7 @@ class FJDXCommon(object):
'volumename: %(volumename)s, '
'Invalid LUNames.',
{'volumename': volumename})
elif rc != 0:
elif rc != CONSTANTS.RC_OK:
msg = (_('_unmap_lun, '
'volumename: %(volumename)s, '
'volume_uid: %(volume_uid)s, '
@ -2847,7 +2859,7 @@ class FJDXCommon(object):
"""Check whether user's role is accessible to ETERNUS and Software."""
ret = True
rc, errordesc, job = self._exec_eternus_cli('check_user_role')
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('_check_user, '
'Return code: %(rc)lu, '
'Error: %(errordesc)s, '
@ -2871,100 +2883,79 @@ class FJDXCommon(object):
def _exec_eternus_cli(self, command, retry=CONSTANTS.TIMES_MIN,
retry_interval=CONSTANTS.RETRY_INTERVAL,
retry_code=[32787], filename=None, timeout=None,
retry_code=['E0060'], filename=None,
**param_dict):
"""Execute ETERNUS CLI."""
LOG.debug('_exec_eternus_cli, '
'command: %(a)s, '
'filename: %(f)s, '
'timeout: %(t)s, '
'parameters: %(b)s.',
{'a': command,
'f': filename,
't': timeout,
'b': param_dict})
result = None
out = None
rc = None
retdata = None
errordesc = None
filename = self.configuration.cinder_eternus_config_file
storage_ip = self._get_drvcfg('EternusIP')
storage_ip = self._get_drvcfg('EternusIP', filename)
if not self.fjdxcli.get(filename):
user = self._get_drvcfg('EternusUser')
password = self._get_drvcfg('EternusPassword')
self.fjdxcli[filename] = (
eternus_dx_cli.FJDXCLI(user, storage_ip,
password=password))
user = self._get_drvcfg('EternusUser', filename)
if self.passwordless:
self.fjdxcli[filename] = (
eternus_dx_cli.FJDXCLI(user,
storage_ip,
keyfile=self.private_key_path))
else:
password = self._get_drvcfg('EternusPassword', filename)
self.fjdxcli[filename] = (
eternus_dx_cli.FJDXCLI(user, storage_ip,
password=password))
for retry_num in range(retry):
# Execute ETERNUS CLI and get return value.
try:
out_dict = self.fjdxcli[filename].done(command, **param_dict)
result = out_dict.get('result')
out = self.fjdxcli[filename].done(command, **param_dict)
out_dict = out
rc_str = out_dict.get('rc')
retdata = out_dict.get('message')
except Exception as ex:
msg = (_('_exec_eternus_cli, '
'stdout: %(out)s, '
'unexpected error: %(ex)s.')
% {'ex': ex})
% {'out': out,
'ex': ex})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Check ssh result.
if result == 255:
LOG.info('_exec_eternus_cli, retry, '
'command: %(command)s, '
'option: %(option)s, '
'ip: %(ip)s, '
'SSH Result: %(result)s, '
'retdata: %(retdata)s, '
'TryNum: %(rn)s.',
{'command': command,
'option': param_dict,
'ip': storage_ip,
'result': result,
'retdata': retdata,
'rn': (retry_num + 1)})
time.sleep(retry_interval)
continue
elif result != 0:
msg = (_('_exec_eternus_cli, '
'unexpected error, '
'command: %(command)s, '
'option: %(option)s, '
'ip: %(ip)s, '
'resuslt: %(result)s, '
'retdata: %(retdata)s.')
% {'command': command,
'option': param_dict,
'ip': storage_ip,
'result': result,
'retdata': retdata})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Check CLI return code.
if rc_str.isdigit():
# SMI-S style return code.
rc = int(rc_str)
try:
errordesc = CONSTANTS.RETCODE_dic[str(rc)]
except Exception:
errordesc = 'Undefined Error!!'
if rc in retry_code:
if rc_str.startswith('E'):
errordesc = rc_str
rc = CONSTANTS.RC_FAILED
if rc_str in retry_code:
LOG.info('_exec_eternus_cli, retry, '
'ip: %(ip)s, '
'RetryCode: %(rc)s, '
'TryNum: %(rn)s.',
{'ip': storage_ip,
'rc': rc,
'rc': rc_str,
'rn': (retry_num + 1)})
time.sleep(retry_interval)
continue
if rc == 4:
else:
LOG.warning('_exec_eternus_cli, '
'WARNING!! '
'ip: %(ip)s, '
'ReturnCode: %(rc_str)s, '
'ReturnData: %(retdata)s.',
{'ip': storage_ip,
'rc_str': rc_str,
'retdata': retdata})
break
else:
if rc_str == str(CONSTANTS.RC_FAILED):
errordesc = rc_str
rc = CONSTANTS.RC_FAILED
if ('Authentication failed' in retdata and
retry_num + 1 < retry):
LOG.warning('_exec_eternus_cli, retry, ip: %(ip)s, '
@ -2975,35 +2966,12 @@ class FJDXCommon(object):
'rn': (retry_num + 1)})
time.sleep(1)
continue
break
else:
# CLI style return code.
LOG.warning('_exec_eternus_cli, '
'WARNING!! '
'ip: %(ip)s, '
'ReturnCode: %(rc_str)s, '
'ReturnData: %(retdata)s.',
{'ip': storage_ip,
'rc_str': rc_str,
'retdata': retdata})
errordesc = rc_str
rc = 4 # Failed.
else:
errordesc = None
rc = CONSTANTS.RC_OK
break
else:
if 0 < result:
msg = (_('_exec_eternus_cli, '
'cannot connect to ETERNUS. '
'SSH Result: %(result)s, '
'retdata: %(retdata)s.')
% {'result': result,
'retdata': retdata})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
else:
LOG.warning('_exec_eternus_cli, Retry was exceeded.')
LOG.warning('_exec_eternus_cli, Retry was exceeded.')
ret = (rc, errordesc, retdata)
@ -3062,7 +3030,7 @@ class FJDXCommon(object):
# Get storage version information.
rc, emsg, clidata = self._exec_eternus_cli('show_enclosure_status')
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('_set_qos, '
'show_enclosure_status failed. '
'Return code: %(rc)lu, '
@ -3115,7 +3083,7 @@ class FJDXCommon(object):
rc, errordesc, job = self._exec_eternus_cli(
'set_volume_qos',
**category_dict)
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('_set_qos, '
'set_volume_qos failed. '
'Return code: %(rc)lu, '
@ -3302,7 +3270,7 @@ class FJDXCommon(object):
# Get all the bandwidth limits.
rc, errordesc, bandwidthlist = self._exec_eternus_cli(
'show_qos_bandwidth_limit')
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('_get_qos_category, '
'show_qos_bandwidth_limit failed. '
'Return code: %(rc)lu, '
@ -3335,7 +3303,7 @@ class FJDXCommon(object):
return ret_dict
rc, errordesc, vqosdatalist = self._exec_eternus_cli('show_volume_qos')
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('_get_qos_category, '
'show_volume_qos failed. '
'Return code: %(rc)lu, '
@ -3427,7 +3395,7 @@ class FJDXCommon(object):
rc, emsg, clidata = self._exec_eternus_cli(
'set_qos_bandwidth_limit', **param_dict)
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('_set_limit, '
'set_qos_bandwidth_limit failed. '
'Return code: %(rc)lu, '
@ -3506,7 +3474,7 @@ class FJDXCommon(object):
WaitForCopyState=self._pywbem_uint(8, '16'),
Synchronization=sdvsession)
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('revert_to_snapshot, '
'_exec_eternus_service error, '
'volume: %(volume)s, '
@ -3541,7 +3509,7 @@ class FJDXCommon(object):
'start_copy_opc',
**param_dict)
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('revert_to_snapshot, '
'start_copy_opc failed. '
'Return code: %(rc)lu, '
@ -3571,7 +3539,7 @@ class FJDXCommon(object):
**param
)
if rc != 0:
if rc != CONSTANTS.RC_OK:
msg = (_('_get_copy_sessions_list, '
'get copy sessions failed. '
'Return code: %(rc)lu, '

View File

@ -102,6 +102,25 @@ Perform the following steps using ETERNUS Web GUI or ETERNUS CLI.
#. Ensure LAN connection between cinder controller and MNT port of ETERNUS DX
and SAN connection between Compute nodes and CA ports of ETERNUS DX.
#. (Optional) If you want to use a public key to SSH to the ETERNUS DX storage,
generate the SSH key, and upload the ``eternus.ietf`` file to the ETERNUS
storage.
For information about how to set the public key, refer to the ETERNUS Web
GUI manuals.
.. code-block:: console
$ ssh-keygen -t rsa -N "" -f ./eternus -m PEM
$ ssh-keygen -e -f ./eternus.pub > ./eternus.ietf
If the public key(eternus.ietf) that was created is deleted by mistake, use
the following command to recreate the key.
.. code-block:: console
$ ssh-keygen -e -f /root/.ssh/eternus.pub > ./eternus.ietf
Configuration
~~~~~~~~~~~~~
@ -219,11 +238,14 @@ Configuration example
volume_driver = cinder.volume.drivers.fujitsu.eternus_dx.eternus_dx_fc.FJDXFCDriver
cinder_eternus_config_file = /etc/cinder/fc.xml
volume_backend_name = FC
fujitsu_passwordless = False
[DXISCSI]
volume_driver = cinder.volume.drivers.fujitsu.eternus_dx.eternus_dx_iscsi.FJDXISCSIDriver
cinder_eternus_config_file = /etc/cinder/iscsi.xml
volume_backend_name = ISCSI
fujitsu_passwordless = True
fujitsu_private_key_path = /etc/cinder/eternus
#. Create the driver configuration files ``fc.xml`` and ``iscsi.xml``.

View File

@ -0,0 +1,40 @@
---
features:
- |
Fujitsu Eternus DX driver: Added support SSH key.
Added the method for connecting to Eternus Storage using SSH key.
The connection method can be selected by setting the value of parameter
``fujitsu_passwordless``, which has a default value of ``True``.
* When ``fujitsu_passwordless`` is set to ``True``, SSH key is used for
connecting to the storage. Additionally, ``fujitsu_private_key_path``
needs to be set to the path of the SSH private key.
* When ``fujitsu_passwordless`` is set to ``False``, password is used for
SSH connection to the storage.
See the `Fujitsu ETERNUS DX driver documentation
<https://docs.openstack.org/cinder/latest/configuration/block-storage/drivers/fujitsu-eternus-dx-driver.html>`_
for details.
upgrade:
- |
Fujitsu Eternus DX driver: Added SSH key and password connection switching
Added the method for connecting to Eternus Storage using SSH key.
The connection method can be selected by setting the value of parameter
``fujitsu_passwordless``, which has a default value of ``True``.
For upgrading from previous versions that relied on password authentication,
you must explicitly set ``fujitsu_passwordless = False`` in the
configuration. This ensures backward compatibility with the legacy
password-based workflow.
The default True value enforces key-based auth for new deployments, aligning
with security best practices at the cost of a minor configuration adjustment
for existing users.
fixes:
- |
Fujitsu Eternus DX driver `bug #2048850
<https://bugs.launchpad.net/cinder/+bug/2048850>`_:
Added parsing of error messages when CLI execution fails.