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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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, '
|
||||
|
||||
@@ -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``.
|
||||
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user