NetApp: Support iSCSI CHAP Uni-directional Auth

This change adds iSCSI CHAP uni-directional authentication support for
NetApp cDOT and 7-Mode iSCSI driver.

Enabling CHAP authentication does not impact an existing iSCSI session.
The iSCSI session needs to be reestablished before CHAP authentication
is initiated.

Co-Authored-By: Dustin Schoenbrun <dustin.schoenbrun@netapp.com>
Co-Authored-By: Alex Meade <mr.alex.meade@gmail.com>

Implements: blueprint netapp-add-chap-authentication-iscsi

DocImpact
Change-Id: I8c481fa09aee02b5472f02819b1a342a3c3e7f71
This commit is contained in:
Chuck Fouts 2015-06-04 10:28:23 -04:00
parent 120f3e21e4
commit ce3052a867
17 changed files with 542 additions and 7 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 attributes.keys() return 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

View File

@ -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 = []

View File

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

View File

@ -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 = []

View File

@ -53,6 +53,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.

View File

@ -0,0 +1,3 @@
---
features:
- Added iSCSI CHAP uni-directional authentication for NetApp drivers.