Merge "NetApp: Support iSCSI CHAP Uni-directional Auth"
This commit is contained in:
commit
8ed2d59395
@ -578,6 +578,7 @@ class NetAppDirectCmodeISCSIDriverTestCase(test.TestCase):
|
|||||||
FakeDirectCmodeHTTPConnection)
|
FakeDirectCmodeHTTPConnection)
|
||||||
driver.do_setup(context='')
|
driver.do_setup(context='')
|
||||||
self.driver = driver
|
self.driver = driver
|
||||||
|
self.mock_object(self.driver.library.zapi_client, '_init_ssh_client')
|
||||||
self.driver.ssc_vols = self.ssc_map
|
self.driver.ssc_vols = self.ssc_map
|
||||||
|
|
||||||
def _set_config(self, configuration):
|
def _set_config(self, configuration):
|
||||||
|
@ -91,6 +91,10 @@ FAKE_RESULT_SUCCESS = netapp_api.NaElement('result')
|
|||||||
FAKE_RESULT_SUCCESS.add_attr('status', 'passed')
|
FAKE_RESULT_SUCCESS.add_attr('status', 'passed')
|
||||||
|
|
||||||
FAKE_HTTP_OPENER = urllib.request.build_opener()
|
FAKE_HTTP_OPENER = urllib.request.build_opener()
|
||||||
|
INITIATOR_IQN = 'iqn.2015-06.com.netapp:fake_iqn'
|
||||||
|
USER_NAME = 'fake_user'
|
||||||
|
PASSWORD = 'passw0rd'
|
||||||
|
ENCRYPTED_PASSWORD = 'B351F145DA527445'
|
||||||
|
|
||||||
NO_RECORDS_RESPONSE = etree.XML("""
|
NO_RECORDS_RESPONSE = etree.XML("""
|
||||||
<results status="passed">
|
<results status="passed">
|
||||||
@ -676,3 +680,12 @@ SYSTEM_GET_INFO_RESPONSE = etree.XML("""
|
|||||||
</system-info>
|
</system-info>
|
||||||
</results>
|
</results>
|
||||||
""" % {'node': NODE_NAME})
|
""" % {'node': NODE_NAME})
|
||||||
|
|
||||||
|
ISCSI_INITIATOR_GET_AUTH_ELEM = etree.XML("""
|
||||||
|
<iscsi-initiator-get-auth>
|
||||||
|
<initiator>%s</initiator>
|
||||||
|
</iscsi-initiator-get-auth>""" % INITIATOR_IQN)
|
||||||
|
|
||||||
|
ISCSI_INITIATOR_AUTH_LIST_INFO_FAILURE = etree.XML("""
|
||||||
|
<results status="failed" errno="13112" reason="Initiator %s not found,
|
||||||
|
please use default authentication." />""" % INITIATOR_IQN)
|
||||||
|
@ -21,6 +21,7 @@ Tests for NetApp API layer
|
|||||||
import ddt
|
import ddt
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
import mock
|
import mock
|
||||||
|
import paramiko
|
||||||
import six
|
import six
|
||||||
from six.moves import urllib
|
from six.moves import urllib
|
||||||
|
|
||||||
@ -507,3 +508,104 @@ class NetAppApiInvokeTests(test.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(zapi_fakes.FAKE_API_NAME_ELEMENT.to_string(),
|
self.assertEqual(zapi_fakes.FAKE_API_NAME_ELEMENT.to_string(),
|
||||||
netapp_api.create_api_request(**params).to_string())
|
netapp_api.create_api_request(**params).to_string())
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
|
class SSHUtilTests(test.TestCase):
|
||||||
|
"""Test Cases for SSH API invocation."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(SSHUtilTests, self).setUp()
|
||||||
|
self.mock_object(netapp_api.SSHUtil, '_init_ssh_pool')
|
||||||
|
self.sshutil = netapp_api.SSHUtil('127.0.0.1',
|
||||||
|
'fake_user',
|
||||||
|
'fake_password')
|
||||||
|
|
||||||
|
def test_execute_command(self):
|
||||||
|
ssh = mock.Mock(paramiko.SSHClient)
|
||||||
|
stdin, stdout, stderr = self._mock_ssh_channel_files(
|
||||||
|
paramiko.ChannelFile)
|
||||||
|
self.mock_object(ssh, 'exec_command',
|
||||||
|
mock.Mock(return_value=(stdin,
|
||||||
|
stdout,
|
||||||
|
stderr)))
|
||||||
|
|
||||||
|
wait_on_stdout = self.mock_object(self.sshutil, '_wait_on_stdout')
|
||||||
|
stdout_read = self.mock_object(stdout, 'read',
|
||||||
|
mock.Mock(return_value=''))
|
||||||
|
self.sshutil.execute_command(ssh, 'ls')
|
||||||
|
|
||||||
|
wait_on_stdout.assert_called_once_with(stdout,
|
||||||
|
netapp_api.SSHUtil.RECV_TIMEOUT)
|
||||||
|
stdout_read.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_execute_read_exception(self):
|
||||||
|
ssh = mock.Mock(paramiko.SSHClient)
|
||||||
|
exec_command = self.mock_object(ssh, 'exec_command')
|
||||||
|
exec_command.side_effect = paramiko.SSHException('Failure')
|
||||||
|
wait_on_stdout = self.mock_object(self.sshutil, '_wait_on_stdout')
|
||||||
|
|
||||||
|
self.assertRaises(paramiko.SSHException,
|
||||||
|
self.sshutil.execute_command, ssh, 'ls')
|
||||||
|
wait_on_stdout.assert_not_called()
|
||||||
|
|
||||||
|
@ddt.data('Password:',
|
||||||
|
'Password: ',
|
||||||
|
'Password: \n\n')
|
||||||
|
def test_execute_command_with_prompt(self, response):
|
||||||
|
ssh = mock.Mock(paramiko.SSHClient)
|
||||||
|
stdin, stdout, stderr = self._mock_ssh_channel_files(paramiko.Channel)
|
||||||
|
stdout_read = self.mock_object(stdout.channel, 'recv',
|
||||||
|
mock.Mock(return_value=response))
|
||||||
|
stdin_write = self.mock_object(stdin, 'write')
|
||||||
|
self.mock_object(ssh, 'exec_command',
|
||||||
|
mock.Mock(return_value=(stdin,
|
||||||
|
stdout,
|
||||||
|
stderr)))
|
||||||
|
|
||||||
|
wait_on_stdout = self.mock_object(self.sshutil, '_wait_on_stdout')
|
||||||
|
self.sshutil.execute_command_with_prompt(ssh, 'sudo ls',
|
||||||
|
'Password:', 'easypass')
|
||||||
|
|
||||||
|
wait_on_stdout.assert_called_once_with(stdout,
|
||||||
|
netapp_api.SSHUtil.RECV_TIMEOUT)
|
||||||
|
stdout_read.assert_called_once_with(999)
|
||||||
|
stdin_write.assert_called_once_with('easypass' + '\n')
|
||||||
|
|
||||||
|
def test_execute_command_unexpected_response(self):
|
||||||
|
ssh = mock.Mock(paramiko.SSHClient)
|
||||||
|
stdin, stdout, stderr = self._mock_ssh_channel_files(paramiko.Channel)
|
||||||
|
stdout_read = self.mock_object(stdout.channel, 'recv',
|
||||||
|
mock.Mock(return_value='bad response'))
|
||||||
|
self.mock_object(ssh, 'exec_command',
|
||||||
|
mock.Mock(return_value=(stdin,
|
||||||
|
stdout,
|
||||||
|
stderr)))
|
||||||
|
|
||||||
|
wait_on_stdout = self.mock_object(self.sshutil, '_wait_on_stdout')
|
||||||
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.sshutil.execute_command_with_prompt,
|
||||||
|
ssh, 'sudo ls', 'Password:', 'easypass')
|
||||||
|
|
||||||
|
wait_on_stdout.assert_called_once_with(stdout,
|
||||||
|
netapp_api.SSHUtil.RECV_TIMEOUT)
|
||||||
|
stdout_read.assert_called_once_with(999)
|
||||||
|
|
||||||
|
def test_wait_on_stdout(self):
|
||||||
|
stdout = mock.Mock()
|
||||||
|
stdout.channel = mock.Mock(paramiko.Channel)
|
||||||
|
|
||||||
|
exit_status = self.mock_object(stdout.channel, 'exit_status_ready',
|
||||||
|
mock.Mock(return_value=False))
|
||||||
|
self.sshutil._wait_on_stdout(stdout, 1)
|
||||||
|
exit_status.assert_any_call()
|
||||||
|
self.assertTrue(exit_status.call_count > 2)
|
||||||
|
|
||||||
|
def _mock_ssh_channel_files(self, channel):
|
||||||
|
stdin = mock.Mock()
|
||||||
|
stdin.channel = mock.Mock(channel)
|
||||||
|
stdout = mock.Mock()
|
||||||
|
stdout.channel = mock.Mock(channel)
|
||||||
|
stderr = mock.Mock()
|
||||||
|
stderr.channel = mock.Mock(channel)
|
||||||
|
return stdin, stdout, stderr
|
||||||
|
@ -18,8 +18,10 @@ import uuid
|
|||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
import mock
|
import mock
|
||||||
|
import paramiko
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from cinder import ssh_utils
|
||||||
from cinder import test
|
from cinder import test
|
||||||
from cinder.tests.unit.volume.drivers.netapp.dataontap.client import (
|
from cinder.tests.unit.volume.drivers.netapp.dataontap.client import (
|
||||||
fakes as fake_client)
|
fakes as fake_client)
|
||||||
@ -42,12 +44,14 @@ class NetApp7modeClientTestCase(test.TestCase):
|
|||||||
|
|
||||||
self.fake_volume = six.text_type(uuid.uuid4())
|
self.fake_volume = six.text_type(uuid.uuid4())
|
||||||
|
|
||||||
|
self.mock_object(client_7mode.Client, '_init_ssh_client')
|
||||||
with mock.patch.object(client_7mode.Client,
|
with mock.patch.object(client_7mode.Client,
|
||||||
'get_ontapi_version',
|
'get_ontapi_version',
|
||||||
return_value=(1, 20)):
|
return_value=(1, 20)):
|
||||||
self.client = client_7mode.Client([self.fake_volume],
|
self.client = client_7mode.Client([self.fake_volume],
|
||||||
**CONNECTION_INFO)
|
**CONNECTION_INFO)
|
||||||
|
|
||||||
|
self.client.ssh_client = mock.MagicMock()
|
||||||
self.client.connection = mock.MagicMock()
|
self.client.connection = mock.MagicMock()
|
||||||
self.connection = self.client.connection
|
self.connection = self.client.connection
|
||||||
self.fake_lun = six.text_type(uuid.uuid4())
|
self.fake_lun = six.text_type(uuid.uuid4())
|
||||||
@ -729,3 +733,38 @@ class NetApp7modeClientTestCase(test.TestCase):
|
|||||||
result = self.client.get_system_name()
|
result = self.client.get_system_name()
|
||||||
|
|
||||||
self.assertEqual(fake_client.NODE_NAME, result)
|
self.assertEqual(fake_client.NODE_NAME, result)
|
||||||
|
|
||||||
|
def test_check_iscsi_initiator_exists_when_no_initiator_exists(self):
|
||||||
|
self.connection.invoke_successfully = mock.Mock(
|
||||||
|
side_effect=netapp_api.NaApiError)
|
||||||
|
initiator = fake_client.INITIATOR_IQN
|
||||||
|
|
||||||
|
initiator_exists = self.client.check_iscsi_initiator_exists(initiator)
|
||||||
|
|
||||||
|
self.assertFalse(initiator_exists)
|
||||||
|
|
||||||
|
def test_check_iscsi_initiator_exists_when_initiator_exists(self):
|
||||||
|
self.connection.invoke_successfully = mock.Mock()
|
||||||
|
initiator = fake_client.INITIATOR_IQN
|
||||||
|
|
||||||
|
initiator_exists = self.client.check_iscsi_initiator_exists(initiator)
|
||||||
|
|
||||||
|
self.assertTrue(initiator_exists)
|
||||||
|
|
||||||
|
def test_set_iscsi_chap_authentication(self):
|
||||||
|
ssh = mock.Mock(paramiko.SSHClient)
|
||||||
|
sshpool = mock.Mock(ssh_utils.SSHPool)
|
||||||
|
self.client.ssh_client.ssh_pool = sshpool
|
||||||
|
self.mock_object(self.client.ssh_client, 'execute_command')
|
||||||
|
sshpool.item().__enter__ = mock.Mock(return_value=ssh)
|
||||||
|
sshpool.item().__exit__ = mock.Mock(return_value=False)
|
||||||
|
|
||||||
|
self.client.set_iscsi_chap_authentication(fake_client.INITIATOR_IQN,
|
||||||
|
fake_client.USER_NAME,
|
||||||
|
fake_client.PASSWORD)
|
||||||
|
|
||||||
|
command = ('iscsi security add -i iqn.2015-06.com.netapp:fake_iqn '
|
||||||
|
'-s CHAP -p passw0rd -n fake_user')
|
||||||
|
self.client.ssh_client.execute_command.assert_has_calls(
|
||||||
|
[mock.call(ssh, command)]
|
||||||
|
)
|
||||||
|
@ -41,8 +41,10 @@ class NetAppBaseClientTestCase(test.TestCase):
|
|||||||
super(NetAppBaseClientTestCase, self).setUp()
|
super(NetAppBaseClientTestCase, self).setUp()
|
||||||
|
|
||||||
self.mock_object(client_base, 'LOG')
|
self.mock_object(client_base, 'LOG')
|
||||||
|
self.mock_object(client_base.Client, '_init_ssh_client')
|
||||||
self.client = client_base.Client(**CONNECTION_INFO)
|
self.client = client_base.Client(**CONNECTION_INFO)
|
||||||
self.client.connection = mock.MagicMock()
|
self.client.connection = mock.MagicMock()
|
||||||
|
self.client.ssh_client = mock.MagicMock()
|
||||||
self.connection = self.client.connection
|
self.connection = self.client.connection
|
||||||
self.fake_volume = six.text_type(uuid.uuid4())
|
self.fake_volume = six.text_type(uuid.uuid4())
|
||||||
self.fake_lun = six.text_type(uuid.uuid4())
|
self.fake_lun = six.text_type(uuid.uuid4())
|
||||||
|
@ -18,9 +18,11 @@ import uuid
|
|||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
import mock
|
import mock
|
||||||
|
import paramiko
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
|
from cinder import ssh_utils
|
||||||
from cinder import test
|
from cinder import test
|
||||||
from cinder.tests.unit.volume.drivers.netapp.dataontap.client import (
|
from cinder.tests.unit.volume.drivers.netapp.dataontap.client import (
|
||||||
fakes as fake_client)
|
fakes as fake_client)
|
||||||
@ -43,13 +45,16 @@ class NetAppCmodeClientTestCase(test.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(NetAppCmodeClientTestCase, self).setUp()
|
super(NetAppCmodeClientTestCase, self).setUp()
|
||||||
|
|
||||||
|
self.mock_object(client_cmode.Client, '_init_ssh_client')
|
||||||
with mock.patch.object(client_cmode.Client,
|
with mock.patch.object(client_cmode.Client,
|
||||||
'get_ontapi_version',
|
'get_ontapi_version',
|
||||||
return_value=(1, 20)):
|
return_value=(1, 20)):
|
||||||
self.client = client_cmode.Client(**CONNECTION_INFO)
|
self.client = client_cmode.Client(**CONNECTION_INFO)
|
||||||
|
|
||||||
|
self.client.ssh_client = mock.MagicMock()
|
||||||
self.client.connection = mock.MagicMock()
|
self.client.connection = mock.MagicMock()
|
||||||
self.connection = self.client.connection
|
self.connection = self.client.connection
|
||||||
|
|
||||||
self.vserver = CONNECTION_INFO['vserver']
|
self.vserver = CONNECTION_INFO['vserver']
|
||||||
self.fake_volume = six.text_type(uuid.uuid4())
|
self.fake_volume = six.text_type(uuid.uuid4())
|
||||||
self.fake_lun = six.text_type(uuid.uuid4())
|
self.fake_lun = six.text_type(uuid.uuid4())
|
||||||
@ -1159,3 +1164,85 @@ class NetAppCmodeClientTestCase(test.TestCase):
|
|||||||
self.mock_send_request.assert_called_once_with(
|
self.mock_send_request.assert_called_once_with(
|
||||||
'perf-object-get-instances', perf_object_get_instances_args,
|
'perf-object-get-instances', perf_object_get_instances_args,
|
||||||
enable_tunneling=False)
|
enable_tunneling=False)
|
||||||
|
|
||||||
|
def test_check_iscsi_initiator_exists_when_no_initiator_exists(self):
|
||||||
|
self.connection.invoke_successfully = mock.Mock(
|
||||||
|
side_effect=netapp_api.NaApiError)
|
||||||
|
initiator = fake_client.INITIATOR_IQN
|
||||||
|
|
||||||
|
initiator_exists = self.client.check_iscsi_initiator_exists(initiator)
|
||||||
|
|
||||||
|
self.assertFalse(initiator_exists)
|
||||||
|
|
||||||
|
def test_check_iscsi_initiator_exists_when_initiator_exists(self):
|
||||||
|
self.connection.invoke_successfully = mock.Mock()
|
||||||
|
initiator = fake_client.INITIATOR_IQN
|
||||||
|
|
||||||
|
initiator_exists = self.client.check_iscsi_initiator_exists(initiator)
|
||||||
|
|
||||||
|
self.assertTrue(initiator_exists)
|
||||||
|
|
||||||
|
def test_set_iscsi_chap_authentication_no_previous_initiator(self):
|
||||||
|
self.connection.invoke_successfully = mock.Mock()
|
||||||
|
self.mock_object(self.client, 'check_iscsi_initiator_exists',
|
||||||
|
mock.Mock(return_value=False))
|
||||||
|
|
||||||
|
ssh = mock.Mock(paramiko.SSHClient)
|
||||||
|
sshpool = mock.Mock(ssh_utils.SSHPool)
|
||||||
|
self.client.ssh_client.ssh_pool = sshpool
|
||||||
|
self.mock_object(self.client.ssh_client, 'execute_command_with_prompt')
|
||||||
|
sshpool.item().__enter__ = mock.Mock(return_value=ssh)
|
||||||
|
sshpool.item().__exit__ = mock.Mock(return_value=False)
|
||||||
|
|
||||||
|
self.client.set_iscsi_chap_authentication(fake_client.INITIATOR_IQN,
|
||||||
|
fake_client.USER_NAME,
|
||||||
|
fake_client.PASSWORD)
|
||||||
|
|
||||||
|
command = ('iscsi security create -vserver fake_vserver '
|
||||||
|
'-initiator-name iqn.2015-06.com.netapp:fake_iqn '
|
||||||
|
'-auth-type CHAP -user-name fake_user')
|
||||||
|
self.client.ssh_client.execute_command_with_prompt.assert_has_calls(
|
||||||
|
[mock.call(ssh, command, 'Password:', fake_client.PASSWORD)]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_set_iscsi_chap_authentication_with_preexisting_initiator(self):
|
||||||
|
self.connection.invoke_successfully = mock.Mock()
|
||||||
|
self.mock_object(self.client, 'check_iscsi_initiator_exists',
|
||||||
|
mock.Mock(return_value=True))
|
||||||
|
|
||||||
|
ssh = mock.Mock(paramiko.SSHClient)
|
||||||
|
sshpool = mock.Mock(ssh_utils.SSHPool)
|
||||||
|
self.client.ssh_client.ssh_pool = sshpool
|
||||||
|
self.mock_object(self.client.ssh_client, 'execute_command_with_prompt')
|
||||||
|
sshpool.item().__enter__ = mock.Mock(return_value=ssh)
|
||||||
|
sshpool.item().__exit__ = mock.Mock(return_value=False)
|
||||||
|
|
||||||
|
self.client.set_iscsi_chap_authentication(fake_client.INITIATOR_IQN,
|
||||||
|
fake_client.USER_NAME,
|
||||||
|
fake_client.PASSWORD)
|
||||||
|
|
||||||
|
command = ('iscsi security modify -vserver fake_vserver '
|
||||||
|
'-initiator-name iqn.2015-06.com.netapp:fake_iqn '
|
||||||
|
'-auth-type CHAP -user-name fake_user')
|
||||||
|
self.client.ssh_client.execute_command_with_prompt.assert_has_calls(
|
||||||
|
[mock.call(ssh, command, 'Password:', fake_client.PASSWORD)]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_set_iscsi_chap_authentication_with_ssh_exception(self):
|
||||||
|
self.connection.invoke_successfully = mock.Mock()
|
||||||
|
self.mock_object(self.client, 'check_iscsi_initiator_exists',
|
||||||
|
mock.Mock(return_value=True))
|
||||||
|
|
||||||
|
ssh = mock.Mock(paramiko.SSHClient)
|
||||||
|
sshpool = mock.Mock(ssh_utils.SSHPool)
|
||||||
|
self.client.ssh_client.ssh_pool = sshpool
|
||||||
|
sshpool.item().__enter__ = mock.Mock(return_value=ssh)
|
||||||
|
sshpool.item().__enter__.side_effect = paramiko.SSHException(
|
||||||
|
'Connection Failure')
|
||||||
|
sshpool.item().__exit__ = mock.Mock(return_value=False)
|
||||||
|
|
||||||
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.client.set_iscsi_chap_authentication,
|
||||||
|
fake_client.INITIATOR_IQN,
|
||||||
|
fake_client.USER_NAME,
|
||||||
|
fake_client.PASSWORD)
|
||||||
|
@ -130,9 +130,12 @@ ISCSI_SERVICE_IQN = 'fake_iscsi_service_iqn'
|
|||||||
|
|
||||||
ISCSI_CONNECTION_PROPERTIES = {
|
ISCSI_CONNECTION_PROPERTIES = {
|
||||||
'data': {
|
'data': {
|
||||||
'auth_method': 'fake',
|
'auth_method': 'fake_method',
|
||||||
'auth_password': 'auth',
|
'auth_password': 'auth',
|
||||||
'auth_username': 'provider',
|
'auth_username': 'provider',
|
||||||
|
'discovery_auth_method': 'fake_method',
|
||||||
|
'discovery_auth_username': 'provider',
|
||||||
|
'discovery_auth_password': 'auth',
|
||||||
'target_discovered': False,
|
'target_discovered': False,
|
||||||
'target_iqn': ISCSI_SERVICE_IQN,
|
'target_iqn': ISCSI_SERVICE_IQN,
|
||||||
'target_lun': 42,
|
'target_lun': 42,
|
||||||
|
@ -78,6 +78,8 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase):
|
|||||||
@mock.patch.object(block_base.NetAppBlockStorageLibrary, 'do_setup')
|
@mock.patch.object(block_base.NetAppBlockStorageLibrary, 'do_setup')
|
||||||
def test_do_setup(self, super_do_setup, mock_do_partner_setup,
|
def test_do_setup(self, super_do_setup, mock_do_partner_setup,
|
||||||
mock_get_root_volume_name):
|
mock_get_root_volume_name):
|
||||||
|
|
||||||
|
self.mock_object(client_base.Client, '_init_ssh_client')
|
||||||
mock_get_root_volume_name.return_value = 'vol0'
|
mock_get_root_volume_name.return_value = 'vol0'
|
||||||
context = mock.Mock()
|
context = mock.Mock()
|
||||||
|
|
||||||
@ -90,6 +92,7 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase):
|
|||||||
@mock.patch.object(client_base.Client, 'get_ontapi_version',
|
@mock.patch.object(client_base.Client, 'get_ontapi_version',
|
||||||
mock.MagicMock(return_value=(1, 20)))
|
mock.MagicMock(return_value=(1, 20)))
|
||||||
def test_do_partner_setup(self):
|
def test_do_partner_setup(self):
|
||||||
|
self.mock_object(client_base.Client, '_init_ssh_client')
|
||||||
self.library.configuration.netapp_partner_backend_name = 'partner'
|
self.library.configuration.netapp_partner_backend_name = 'partner'
|
||||||
|
|
||||||
self.library._do_partner_setup()
|
self.library._do_partner_setup()
|
||||||
@ -99,7 +102,7 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase):
|
|||||||
@mock.patch.object(client_base.Client, 'get_ontapi_version',
|
@mock.patch.object(client_base.Client, 'get_ontapi_version',
|
||||||
mock.MagicMock(return_value=(1, 20)))
|
mock.MagicMock(return_value=(1, 20)))
|
||||||
def test_do_partner_setup_no_partner(self):
|
def test_do_partner_setup_no_partner(self):
|
||||||
|
self.mock_object(client_base.Client, '_init_ssh_client')
|
||||||
self.library._do_partner_setup()
|
self.library._do_partner_setup()
|
||||||
|
|
||||||
self.assertFalse(hasattr(self.library, 'partner_zapi_client'))
|
self.assertFalse(hasattr(self.library, 'partner_zapi_client'))
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
# Copyright (c) 2014 Andrew Kerr. All rights reserved.
|
# Copyright (c) 2014 Andrew Kerr. All rights reserved.
|
||||||
# Copyright (c) 2015 Tom Barron. All rights reserved.
|
# Copyright (c) 2015 Tom Barron. All rights reserved.
|
||||||
# Copyright (c) 2015 Goutham Pacha Ravi. All rights reserved.
|
# Copyright (c) 2015 Goutham Pacha Ravi. All rights reserved.
|
||||||
|
# Copyright (c) 2015 Dustin Schoenbrun. All rights reserved.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
@ -524,6 +525,27 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase):
|
|||||||
target_info = self.library.initialize_connection_iscsi(volume,
|
target_info = self.library.initialize_connection_iscsi(volume,
|
||||||
connector)
|
connector)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
fake.ISCSI_CONNECTION_PROPERTIES['data']['auth_method'],
|
||||||
|
target_info['data']['auth_method'])
|
||||||
|
self.assertEqual(
|
||||||
|
fake.ISCSI_CONNECTION_PROPERTIES['data']['auth_password'],
|
||||||
|
target_info['data']['auth_password'])
|
||||||
|
self.assertTrue('auth_password' in target_info['data'])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
fake.ISCSI_CONNECTION_PROPERTIES['data']['discovery_auth_method'],
|
||||||
|
target_info['data']['discovery_auth_method'])
|
||||||
|
self.assertEqual(
|
||||||
|
fake.ISCSI_CONNECTION_PROPERTIES['data']
|
||||||
|
['discovery_auth_password'],
|
||||||
|
target_info['data']['discovery_auth_password'])
|
||||||
|
self.assertTrue('auth_password' in target_info['data'])
|
||||||
|
self.assertEqual(
|
||||||
|
fake.ISCSI_CONNECTION_PROPERTIES['data']
|
||||||
|
['discovery_auth_username'],
|
||||||
|
target_info['data']['discovery_auth_username'])
|
||||||
|
|
||||||
self.assertEqual(fake.ISCSI_CONNECTION_PROPERTIES, target_info)
|
self.assertEqual(fake.ISCSI_CONNECTION_PROPERTIES, target_info)
|
||||||
block_base.NetAppBlockStorageLibrary._map_lun.assert_called_once_with(
|
block_base.NetAppBlockStorageLibrary._map_lun.assert_called_once_with(
|
||||||
fake.ISCSI_VOLUME['name'], [fake.ISCSI_CONNECTOR['initiator']],
|
fake.ISCSI_VOLUME['name'], [fake.ISCSI_CONNECTOR['initiator']],
|
||||||
@ -666,8 +688,10 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase):
|
|||||||
self.library.configuration.netapp_lun_ostype = 'linux'
|
self.library.configuration.netapp_lun_ostype = 'linux'
|
||||||
self.library.configuration.netapp_host_type = 'future_os'
|
self.library.configuration.netapp_host_type = 'future_os'
|
||||||
self.library.do_setup(mock.Mock())
|
self.library.do_setup(mock.Mock())
|
||||||
|
|
||||||
self.assertRaises(exception.NetAppDriverException,
|
self.assertRaises(exception.NetAppDriverException,
|
||||||
self.library.check_for_setup_error)
|
self.library.check_for_setup_error)
|
||||||
|
|
||||||
msg = _("Invalid value for NetApp configuration"
|
msg = _("Invalid value for NetApp configuration"
|
||||||
" option netapp_host_type.")
|
" option netapp_host_type.")
|
||||||
block_base.LOG.error.assert_called_once_with(msg)
|
block_base.LOG.error.assert_called_once_with(msg)
|
||||||
@ -998,3 +1022,28 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase):
|
|||||||
self.assertFalse(mock_get_lun_geometry.called)
|
self.assertFalse(mock_get_lun_geometry.called)
|
||||||
self.assertFalse(mock_do_direct_resize.called)
|
self.assertFalse(mock_do_direct_resize.called)
|
||||||
self.assertFalse(mock_do_sub_clone_resize.called)
|
self.assertFalse(mock_do_sub_clone_resize.called)
|
||||||
|
|
||||||
|
def test_configure_chap_generate_username_and_password(self):
|
||||||
|
"""Ensure that a CHAP username and password are generated."""
|
||||||
|
initiator_name = fake.ISCSI_CONNECTOR['initiator']
|
||||||
|
|
||||||
|
username, password = self.library._configure_chap(initiator_name)
|
||||||
|
|
||||||
|
self.assertEqual(na_utils.DEFAULT_CHAP_USER_NAME, username)
|
||||||
|
self.assertIsNotNone(password)
|
||||||
|
self.assertEqual(len(password), na_utils.CHAP_SECRET_LENGTH)
|
||||||
|
|
||||||
|
def test_add_chap_properties(self):
|
||||||
|
"""Ensure that CHAP properties are added to the properties dictionary
|
||||||
|
|
||||||
|
"""
|
||||||
|
properties = {'data': {}}
|
||||||
|
self.library._add_chap_properties(properties, 'user1', 'pass1')
|
||||||
|
|
||||||
|
data = properties['data']
|
||||||
|
self.assertEqual('CHAP', data['auth_method'])
|
||||||
|
self.assertEqual('user1', data['auth_username'])
|
||||||
|
self.assertEqual('pass1', data['auth_password'])
|
||||||
|
self.assertEqual('CHAP', data['discovery_auth_method'])
|
||||||
|
self.assertEqual('user1', data['discovery_auth_username'])
|
||||||
|
self.assertEqual('pass1', data['discovery_auth_password'])
|
||||||
|
@ -81,6 +81,7 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
|
|||||||
@mock.patch.object(na_utils, 'check_flags')
|
@mock.patch.object(na_utils, 'check_flags')
|
||||||
@mock.patch.object(block_base.NetAppBlockStorageLibrary, 'do_setup')
|
@mock.patch.object(block_base.NetAppBlockStorageLibrary, 'do_setup')
|
||||||
def test_do_setup(self, super_do_setup, mock_check_flags):
|
def test_do_setup(self, super_do_setup, mock_check_flags):
|
||||||
|
self.mock_object(client_base.Client, '_init_ssh_client')
|
||||||
context = mock.Mock()
|
context = mock.Mock()
|
||||||
|
|
||||||
self.library.do_setup(context)
|
self.library.do_setup(context)
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
# Copyright (c) 2014 Andrew Kerr. All rights reserved.
|
# Copyright (c) 2014 Andrew Kerr. All rights reserved.
|
||||||
# Copyright (c) 2014 Jeff Applewhite. All rights reserved.
|
# Copyright (c) 2014 Jeff Applewhite. All rights reserved.
|
||||||
# Copyright (c) 2015 Tom Barron. All rights reserved.
|
# Copyright (c) 2015 Tom Barron. All rights reserved.
|
||||||
|
# Copyright (c) 2015 Chuck Fouts. All rights reserved.
|
||||||
|
# Copyright (c) 2015 Dustin Schoenbrun. All rights reserved.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
# not use this file except in compliance with the License. You may obtain
|
# not use this file except in compliance with the License. You may obtain
|
||||||
@ -42,7 +44,6 @@ from cinder.volume.drivers.netapp import utils as na_utils
|
|||||||
from cinder.volume import utils as volume_utils
|
from cinder.volume import utils as volume_utils
|
||||||
from cinder.zonemanager import utils as fczm_utils
|
from cinder.zonemanager import utils as fczm_utils
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -778,8 +779,32 @@ class NetAppBlockStorageLibrary(object):
|
|||||||
properties = na_utils.get_iscsi_connection_properties(lun_id, volume,
|
properties = na_utils.get_iscsi_connection_properties(lun_id, volume,
|
||||||
iqn, address,
|
iqn, address,
|
||||||
port)
|
port)
|
||||||
|
|
||||||
|
if self.configuration.use_chap_auth:
|
||||||
|
chap_username, chap_password = self._configure_chap(initiator_name)
|
||||||
|
self._add_chap_properties(properties, chap_username, chap_password)
|
||||||
|
|
||||||
return properties
|
return properties
|
||||||
|
|
||||||
|
def _configure_chap(self, initiator_name):
|
||||||
|
password = volume_utils.generate_password(na_utils.CHAP_SECRET_LENGTH)
|
||||||
|
username = na_utils.DEFAULT_CHAP_USER_NAME
|
||||||
|
|
||||||
|
self.zapi_client.set_iscsi_chap_authentication(initiator_name,
|
||||||
|
username,
|
||||||
|
password)
|
||||||
|
LOG.debug("Set iSCSI CHAP authentication.")
|
||||||
|
|
||||||
|
return username, password
|
||||||
|
|
||||||
|
def _add_chap_properties(self, properties, username, password):
|
||||||
|
properties['data']['auth_method'] = 'CHAP'
|
||||||
|
properties['data']['auth_username'] = username
|
||||||
|
properties['data']['auth_password'] = password
|
||||||
|
properties['data']['discovery_auth_method'] = 'CHAP'
|
||||||
|
properties['data']['discovery_auth_username'] = username
|
||||||
|
properties['data']['discovery_auth_password'] = password
|
||||||
|
|
||||||
def _get_preferred_target_from_list(self, target_details_list,
|
def _get_preferred_target_from_list(self, target_details_list,
|
||||||
filter=None):
|
filter=None):
|
||||||
preferred_target = None
|
preferred_target = None
|
||||||
|
@ -22,14 +22,18 @@ Contains classes required to issue API calls to Data ONTAP and OnCommand DFM.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
from eventlet import greenthread
|
||||||
|
from eventlet import semaphore
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
import random
|
||||||
import six
|
import six
|
||||||
from six.moves import urllib
|
from six.moves import urllib
|
||||||
|
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
|
from cinder import ssh_utils
|
||||||
from cinder import utils
|
from cinder import utils
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -393,7 +397,7 @@ class NaElement(object):
|
|||||||
return list(attributes.keys())
|
return list(attributes.keys())
|
||||||
|
|
||||||
def add_new_child(self, name, content, convert=False):
|
def add_new_child(self, name, content, convert=False):
|
||||||
"""Add child with tag name and context.
|
"""Add child with tag name and content.
|
||||||
|
|
||||||
Convert replaces entity refs to chars.
|
Convert replaces entity refs to chars.
|
||||||
"""
|
"""
|
||||||
@ -434,6 +438,12 @@ class NaElement(object):
|
|||||||
xml = xml.decode('utf-8')
|
xml = xml.decode('utf-8')
|
||||||
return xml
|
return xml
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return str(self) == str(other)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(str(self))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self)
|
return str(self)
|
||||||
|
|
||||||
@ -617,3 +627,84 @@ def create_api_request(api_name, query=None, des_result=None,
|
|||||||
if tag:
|
if tag:
|
||||||
api_el.add_new_child('tag', tag, True)
|
api_el.add_new_child('tag', tag, True)
|
||||||
return api_el
|
return api_el
|
||||||
|
|
||||||
|
|
||||||
|
class SSHUtil(object):
|
||||||
|
"""Encapsulates connection logic and command execution for SSH client."""
|
||||||
|
|
||||||
|
MAX_CONCURRENT_SSH_CONNECTIONS = 5
|
||||||
|
RECV_TIMEOUT = 3
|
||||||
|
CONNECTION_KEEP_ALIVE = 600
|
||||||
|
WAIT_ON_STDOUT_TIMEOUT = 3
|
||||||
|
|
||||||
|
def __init__(self, host, username, password, port=22):
|
||||||
|
self.ssh_pool = self._init_ssh_pool(host, port, username, password)
|
||||||
|
|
||||||
|
# Note(cfouts) Number of SSH connections made to the backend need to be
|
||||||
|
# limited. Use of SSHPool allows connections to be cached and reused
|
||||||
|
# instead of creating a new connection each time a command is executed
|
||||||
|
# via SSH.
|
||||||
|
self.ssh_connect_semaphore = semaphore.Semaphore(
|
||||||
|
self.MAX_CONCURRENT_SSH_CONNECTIONS)
|
||||||
|
|
||||||
|
def _init_ssh_pool(self, host, port, username, password):
|
||||||
|
return ssh_utils.SSHPool(host,
|
||||||
|
port,
|
||||||
|
self.CONNECTION_KEEP_ALIVE,
|
||||||
|
username,
|
||||||
|
password)
|
||||||
|
|
||||||
|
def execute_command(self, client, command_text, timeout=RECV_TIMEOUT):
|
||||||
|
LOG.debug("execute_command() - Sending command.")
|
||||||
|
stdin, stdout, stderr = client.exec_command(command_text)
|
||||||
|
stdin.close()
|
||||||
|
self._wait_on_stdout(stdout, timeout)
|
||||||
|
output = stdout.read()
|
||||||
|
LOG.debug("Output of length %(size)d received.",
|
||||||
|
{'size': len(output)})
|
||||||
|
stdout.close()
|
||||||
|
stderr.close()
|
||||||
|
return output
|
||||||
|
|
||||||
|
def execute_command_with_prompt(self,
|
||||||
|
client,
|
||||||
|
command,
|
||||||
|
expected_prompt_text,
|
||||||
|
prompt_response,
|
||||||
|
timeout=RECV_TIMEOUT):
|
||||||
|
LOG.debug("execute_command_with_prompt() - Sending command.")
|
||||||
|
stdin, stdout, stderr = client.exec_command(command)
|
||||||
|
self._wait_on_stdout(stdout, timeout)
|
||||||
|
response = stdout.channel.recv(999)
|
||||||
|
if response.strip() != expected_prompt_text:
|
||||||
|
msg = _("Unexpected output. Expected [%(expected)s] but "
|
||||||
|
"received [%(output)s]") % {
|
||||||
|
'expected': expected_prompt_text,
|
||||||
|
'output': response.strip(),
|
||||||
|
}
|
||||||
|
LOG.error(msg)
|
||||||
|
stdin.close()
|
||||||
|
stdout.close()
|
||||||
|
stderr.close()
|
||||||
|
raise exception.VolumeBackendAPIException(msg)
|
||||||
|
else:
|
||||||
|
LOG.debug("execute_command_with_prompt() - Sending answer")
|
||||||
|
stdin.write(prompt_response + '\n')
|
||||||
|
stdin.flush()
|
||||||
|
stdin.close()
|
||||||
|
stdout.close()
|
||||||
|
stderr.close()
|
||||||
|
|
||||||
|
def _wait_on_stdout(self, stdout, timeout=WAIT_ON_STDOUT_TIMEOUT):
|
||||||
|
wait_time = 0.0
|
||||||
|
# NOTE(cfouts): The server does not always indicate when EOF is reached
|
||||||
|
# for stdout. The timeout exists for this reason and an attempt is made
|
||||||
|
# to read from stdout.
|
||||||
|
while not stdout.channel.exit_status_ready():
|
||||||
|
# period is 10 - 25 centiseconds
|
||||||
|
period = random.randint(10, 25) / 100.0
|
||||||
|
greenthread.sleep(period)
|
||||||
|
wait_time += period
|
||||||
|
if wait_time > timeout:
|
||||||
|
LOG.debug("Timeout exceeded while waiting for exit status.")
|
||||||
|
break
|
||||||
|
@ -109,6 +109,18 @@ class Client(client_base.Client):
|
|||||||
tgt_list.append(d)
|
tgt_list.append(d)
|
||||||
return tgt_list
|
return tgt_list
|
||||||
|
|
||||||
|
def check_iscsi_initiator_exists(self, iqn):
|
||||||
|
"""Returns True if initiator exists."""
|
||||||
|
initiator_exists = True
|
||||||
|
try:
|
||||||
|
auth_list = netapp_api.NaElement('iscsi-initiator-auth-list-info')
|
||||||
|
auth_list.add_new_child('initiator', iqn)
|
||||||
|
self.connection.invoke_successfully(auth_list, True)
|
||||||
|
except netapp_api.NaApiError:
|
||||||
|
initiator_exists = False
|
||||||
|
|
||||||
|
return initiator_exists
|
||||||
|
|
||||||
def get_fc_target_wwpns(self):
|
def get_fc_target_wwpns(self):
|
||||||
"""Gets the FC target details."""
|
"""Gets the FC target details."""
|
||||||
wwpns = []
|
wwpns = []
|
||||||
@ -127,6 +139,31 @@ class Client(client_base.Client):
|
|||||||
result = self.connection.invoke_successfully(iscsi_service_iter, True)
|
result = self.connection.invoke_successfully(iscsi_service_iter, True)
|
||||||
return result.get_child_content('node-name')
|
return result.get_child_content('node-name')
|
||||||
|
|
||||||
|
def set_iscsi_chap_authentication(self, iqn, username, password):
|
||||||
|
"""Provides NetApp host's CHAP credentials to the backend."""
|
||||||
|
|
||||||
|
command = ("iscsi security add -i %(iqn)s -s CHAP "
|
||||||
|
"-p %(password)s -n %(username)s") % {
|
||||||
|
'iqn': iqn,
|
||||||
|
'password': password,
|
||||||
|
'username': username,
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.debug('Updating CHAP authentication for %(iqn)s.', {'iqn': iqn})
|
||||||
|
|
||||||
|
try:
|
||||||
|
ssh_pool = self.ssh_client.ssh_pool
|
||||||
|
with ssh_pool.item() as ssh:
|
||||||
|
self.ssh_client.execute_command(ssh, command)
|
||||||
|
except Exception as e:
|
||||||
|
msg = _('Failed to set CHAP authentication for target IQN '
|
||||||
|
'%(iqn)s. Details: %(ex)s') % {
|
||||||
|
'iqn': iqn,
|
||||||
|
'ex': e,
|
||||||
|
}
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
def get_lun_list(self):
|
def get_lun_list(self):
|
||||||
"""Gets the list of LUNs on filer."""
|
"""Gets the list of LUNs on filer."""
|
||||||
lun_list = []
|
lun_list = []
|
||||||
|
@ -38,12 +38,23 @@ LOG = logging.getLogger(__name__)
|
|||||||
class Client(object):
|
class Client(object):
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
host = kwargs['hostname']
|
||||||
|
username = kwargs['username']
|
||||||
|
password = kwargs['password']
|
||||||
self.connection = netapp_api.NaServer(
|
self.connection = netapp_api.NaServer(
|
||||||
host=kwargs['hostname'],
|
host=host,
|
||||||
transport_type=kwargs['transport_type'],
|
transport_type=kwargs['transport_type'],
|
||||||
port=kwargs['port'],
|
port=kwargs['port'],
|
||||||
username=kwargs['username'],
|
username=username,
|
||||||
password=kwargs['password'])
|
password=password)
|
||||||
|
|
||||||
|
self.ssh_client = self._init_ssh_client(host, username, password)
|
||||||
|
|
||||||
|
def _init_ssh_client(self, host, username, password):
|
||||||
|
return netapp_api.SSHUtil(
|
||||||
|
host=host,
|
||||||
|
username=username,
|
||||||
|
password=password)
|
||||||
|
|
||||||
def _init_features(self):
|
def _init_features(self):
|
||||||
"""Set up the repository of available Data ONTAP features."""
|
"""Set up the repository of available Data ONTAP features."""
|
||||||
@ -231,6 +242,14 @@ class Client(object):
|
|||||||
"""Returns iscsi iqn."""
|
"""Returns iscsi iqn."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def check_iscsi_initiator_exists(self, iqn):
|
||||||
|
"""Returns True if initiator exists."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def set_iscsi_chap_authentication(self, iqn, username, password):
|
||||||
|
"""Provides NetApp host's CHAP credentials to the backend."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_lun_list(self):
|
def get_lun_list(self):
|
||||||
"""Gets the list of LUNs on filer."""
|
"""Gets the list of LUNs on filer."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
@ -91,6 +91,62 @@ class Client(client_base.Client):
|
|||||||
tgt_list.append(d)
|
tgt_list.append(d)
|
||||||
return tgt_list
|
return tgt_list
|
||||||
|
|
||||||
|
def set_iscsi_chap_authentication(self, iqn, username, password):
|
||||||
|
"""Provides NetApp host's CHAP credentials to the backend."""
|
||||||
|
initiator_exists = self.check_iscsi_initiator_exists(iqn)
|
||||||
|
|
||||||
|
command_template = ('iscsi security %(mode)s -vserver %(vserver)s '
|
||||||
|
'-initiator-name %(iqn)s -auth-type CHAP '
|
||||||
|
'-user-name %(username)s')
|
||||||
|
|
||||||
|
if initiator_exists:
|
||||||
|
LOG.debug('Updating CHAP authentication for %(iqn)s.',
|
||||||
|
{'iqn': iqn})
|
||||||
|
command = command_template % {
|
||||||
|
'mode': 'modify',
|
||||||
|
'vserver': self.vserver,
|
||||||
|
'iqn': iqn,
|
||||||
|
'username': username,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
LOG.debug('Adding initiator %(iqn)s with CHAP authentication.',
|
||||||
|
{'iqn': iqn})
|
||||||
|
command = command_template % {
|
||||||
|
'mode': 'create',
|
||||||
|
'vserver': self.vserver,
|
||||||
|
'iqn': iqn,
|
||||||
|
'username': username,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with self.ssh_client.ssh_connect_semaphore:
|
||||||
|
ssh_pool = self.ssh_client.ssh_pool
|
||||||
|
with ssh_pool.item() as ssh:
|
||||||
|
self.ssh_client.execute_command_with_prompt(ssh,
|
||||||
|
command,
|
||||||
|
'Password:',
|
||||||
|
password)
|
||||||
|
except Exception as e:
|
||||||
|
msg = _('Failed to set CHAP authentication for target IQN %(iqn)s.'
|
||||||
|
' Details: %(ex)s') % {
|
||||||
|
'iqn': iqn,
|
||||||
|
'ex': e,
|
||||||
|
}
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
def check_iscsi_initiator_exists(self, iqn):
|
||||||
|
"""Returns True if initiator exists."""
|
||||||
|
initiator_exists = True
|
||||||
|
try:
|
||||||
|
auth_list = netapp_api.NaElement('iscsi-initiator-get-auth')
|
||||||
|
auth_list.add_new_child('initiator', iqn)
|
||||||
|
self.connection.invoke_successfully(auth_list, True)
|
||||||
|
except netapp_api.NaApiError:
|
||||||
|
initiator_exists = False
|
||||||
|
|
||||||
|
return initiator_exists
|
||||||
|
|
||||||
def get_fc_target_wwpns(self):
|
def get_fc_target_wwpns(self):
|
||||||
"""Gets the FC target details."""
|
"""Gets the FC target details."""
|
||||||
wwpns = []
|
wwpns = []
|
||||||
|
@ -54,6 +54,10 @@ DEPRECATED_SSC_SPECS = {'netapp_unmirrored': 'netapp_mirrored',
|
|||||||
QOS_KEYS = frozenset(['maxIOPS', 'maxIOPSperGiB', 'maxBPS', 'maxBPSperGiB'])
|
QOS_KEYS = frozenset(['maxIOPS', 'maxIOPSperGiB', 'maxBPS', 'maxBPSperGiB'])
|
||||||
BACKEND_QOS_CONSUMERS = frozenset(['back-end', 'both'])
|
BACKEND_QOS_CONSUMERS = frozenset(['back-end', 'both'])
|
||||||
|
|
||||||
|
# Secret length cannot be less than 96 bits. http://tools.ietf.org/html/rfc3723
|
||||||
|
CHAP_SECRET_LENGTH = 16
|
||||||
|
DEFAULT_CHAP_USER_NAME = 'NetApp_iSCSI_CHAP_Username'
|
||||||
|
|
||||||
|
|
||||||
def validate_instantiation(**kwargs):
|
def validate_instantiation(**kwargs):
|
||||||
"""Checks if a driver is instantiated other than by the unified driver.
|
"""Checks if a driver is instantiated other than by the unified driver.
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added iSCSI CHAP uni-directional authentication for NetApp drivers.
|
Loading…
Reference in New Issue
Block a user