Fujitsu Driver: Improve CLI function

When the CLI command execution encounters an error, the storage
returns an error code.
However, only some error codes could be parsed, resulting the
inability to view specific error information in the log.
To fix this issue, the parsing of error codes has been added when
the CLI encounters an error, allowing the error information to be
viewed in the log.

In addition, this patch also adds support for SSH keys,
allowing connections to the storage using SSH key.

Closes-Bug: #2048850
Change-Id: I204f3889e3401b142dd7c9a6e11585639585abfc
This commit is contained in:
inori
2024-01-31 02:32:42 -05:00
committed by xuq.fnstxz
parent e953beffb6
commit 6adc73299b
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.