FakeDirectCmodeHTTPConnection)
driver.do_setup(context='')
self.driver = driver
+ self.mock_object(self.driver.library.zapi_client, '_init_ssh_client')
self.driver.ssc_vols = self.ssc_map
def _set_config(self, configuration):
FAKE_RESULT_SUCCESS.add_attr('status', 'passed')
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("""
<results status="passed">
</system-info>
</results>
""" % {'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)
import ddt
from lxml import etree
import mock
+import paramiko
import six
from six.moves import urllib
self.assertEqual(zapi_fakes.FAKE_API_NAME_ELEMENT.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
from lxml import etree
import mock
+import paramiko
import six
+from cinder import ssh_utils
from cinder import test
from cinder.tests.unit.volume.drivers.netapp.dataontap.client import (
fakes as fake_client)
self.fake_volume = six.text_type(uuid.uuid4())
+ self.mock_object(client_7mode.Client, '_init_ssh_client')
with mock.patch.object(client_7mode.Client,
'get_ontapi_version',
return_value=(1, 20)):
self.client = client_7mode.Client([self.fake_volume],
**CONNECTION_INFO)
+ self.client.ssh_client = mock.MagicMock()
self.client.connection = mock.MagicMock()
self.connection = self.client.connection
self.fake_lun = six.text_type(uuid.uuid4())
result = self.client.get_system_name()
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)]
+ )
super(NetAppBaseClientTestCase, self).setUp()
self.mock_object(client_base, 'LOG')
+ self.mock_object(client_base.Client, '_init_ssh_client')
self.client = client_base.Client(**CONNECTION_INFO)
self.client.connection = mock.MagicMock()
+ self.client.ssh_client = mock.MagicMock()
self.connection = self.client.connection
self.fake_volume = six.text_type(uuid.uuid4())
self.fake_lun = six.text_type(uuid.uuid4())
from lxml import etree
import mock
+import paramiko
import six
from cinder import exception
+from cinder import ssh_utils
from cinder import test
from cinder.tests.unit.volume.drivers.netapp.dataontap.client import (
fakes as fake_client)
def setUp(self):
super(NetAppCmodeClientTestCase, self).setUp()
+ self.mock_object(client_cmode.Client, '_init_ssh_client')
with mock.patch.object(client_cmode.Client,
'get_ontapi_version',
return_value=(1, 20)):
self.client = client_cmode.Client(**CONNECTION_INFO)
+ self.client.ssh_client = mock.MagicMock()
self.client.connection = mock.MagicMock()
self.connection = self.client.connection
+
self.vserver = CONNECTION_INFO['vserver']
self.fake_volume = six.text_type(uuid.uuid4())
self.fake_lun = six.text_type(uuid.uuid4())
self.mock_send_request.assert_called_once_with(
'perf-object-get-instances', perf_object_get_instances_args,
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)
ISCSI_CONNECTION_PROPERTIES = {
'data': {
- 'auth_method': 'fake',
+ 'auth_method': 'fake_method',
'auth_password': 'auth',
'auth_username': 'provider',
+ 'discovery_auth_method': 'fake_method',
+ 'discovery_auth_username': 'provider',
+ 'discovery_auth_password': 'auth',
'target_discovered': False,
'target_iqn': ISCSI_SERVICE_IQN,
'target_lun': 42,
@mock.patch.object(block_base.NetAppBlockStorageLibrary, 'do_setup')
def test_do_setup(self, super_do_setup, mock_do_partner_setup,
mock_get_root_volume_name):
+
+ self.mock_object(client_base.Client, '_init_ssh_client')
mock_get_root_volume_name.return_value = 'vol0'
context = mock.Mock()
@mock.patch.object(client_base.Client, 'get_ontapi_version',
mock.MagicMock(return_value=(1, 20)))
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._do_partner_setup()
@mock.patch.object(client_base.Client, 'get_ontapi_version',
mock.MagicMock(return_value=(1, 20)))
def test_do_partner_setup_no_partner(self):
-
+ self.mock_object(client_base.Client, '_init_ssh_client')
self.library._do_partner_setup()
self.assertFalse(hasattr(self.library, 'partner_zapi_client'))
# Copyright (c) 2014 Andrew Kerr. All rights reserved.
# Copyright (c) 2015 Tom Barron. All rights reserved.
# Copyright (c) 2015 Goutham Pacha Ravi. All rights reserved.
+# Copyright (c) 2015 Dustin Schoenbrun. All rights reserved.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
target_info = self.library.initialize_connection_iscsi(volume,
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)
block_base.NetAppBlockStorageLibrary._map_lun.assert_called_once_with(
fake.ISCSI_VOLUME['name'], [fake.ISCSI_CONNECTOR['initiator']],
self.library.configuration.netapp_lun_ostype = 'linux'
self.library.configuration.netapp_host_type = 'future_os'
self.library.do_setup(mock.Mock())
+
self.assertRaises(exception.NetAppDriverException,
self.library.check_for_setup_error)
+
msg = _("Invalid value for NetApp configuration"
" option netapp_host_type.")
block_base.LOG.error.assert_called_once_with(msg)
self.assertFalse(mock_get_lun_geometry.called)
self.assertFalse(mock_do_direct_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'])
@mock.patch.object(na_utils, 'check_flags')
@mock.patch.object(block_base.NetAppBlockStorageLibrary, 'do_setup')
def test_do_setup(self, super_do_setup, mock_check_flags):
+ self.mock_object(client_base.Client, '_init_ssh_client')
context = mock.Mock()
self.library.do_setup(context)
# Copyright (c) 2014 Andrew Kerr. All rights reserved.
# Copyright (c) 2014 Jeff Applewhite. 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
# not use this file except in compliance with the License. You may obtain
from cinder.volume import utils as volume_utils
from cinder.zonemanager import utils as fczm_utils
-
LOG = logging.getLogger(__name__)
properties = na_utils.get_iscsi_connection_properties(lun_id, volume,
iqn, address,
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
+ 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,
filter=None):
preferred_target = None
"""
import copy
+from eventlet import greenthread
+from eventlet import semaphore
from lxml import etree
from oslo_log import log as logging
+import random
import six
from six.moves import urllib
from cinder import exception
from cinder.i18n import _
+from cinder import ssh_utils
from cinder import utils
LOG = logging.getLogger(__name__)
return attributes.keys()
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.
"""
xml = xml.decode('utf-8')
return xml
+ def __eq__(self, other):
+ return str(self) == str(other)
+
+ def __hash__(self):
+ return hash(str(self))
+
def __repr__(self):
return str(self)
if tag:
api_el.add_new_child('tag', tag, True)
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
tgt_list.append(d)
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):
"""Gets the FC target details."""
wwpns = []
result = self.connection.invoke_successfully(iscsi_service_iter, True)
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):
"""Gets the list of LUNs on filer."""
lun_list = []
class Client(object):
def __init__(self, **kwargs):
+ host = kwargs['hostname']
+ username = kwargs['username']
+ password = kwargs['password']
self.connection = netapp_api.NaServer(
- host=kwargs['hostname'],
+ host=host,
transport_type=kwargs['transport_type'],
port=kwargs['port'],
- username=kwargs['username'],
- password=kwargs['password'])
+ username=username,
+ 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):
"""Set up the repository of available Data ONTAP features."""
"""Returns iscsi iqn."""
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):
"""Gets the list of LUNs on filer."""
raise NotImplementedError()
tgt_list.append(d)
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):
"""Gets the FC target details."""
wwpns = []
QOS_KEYS = frozenset(['maxIOPS', 'maxIOPSperGiB', 'maxBPS', 'maxBPSperGiB'])
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):
"""Checks if a driver is instantiated other than by the unified driver.
--- /dev/null
+---
+features:
+ - Added iSCSI CHAP uni-directional authentication for NetApp drivers.