[NetApp] Fix security service configuration for LDAP servers

This patch fixes some issues with LDAP client configuration on
ONTAP SVMs. With ldap security service, users should be able to
configure a LDAP client that can be used for authentication and
name mapping. The name service switch order remains: ldap,files.
Issues fixed:
- The driver now identifies when user provide a Active Directory
  domain or a Linux/Unix LDAP server IP and sets the correct schema.
- LDAP configuration parameter `servers` was replaced by `ldap-servers`
  in ONTAP 9.2, and now accepts host names too.
- Fix DNS configuration for LDAP security service
- User can now specify base search DN for LDAP queries, which can be
  mandatory for Unix/Linux servers, using the security service `ou`
  parameter.

Closes-Bug: #1916534
Change-Id: Ieaa53abbe50e7b708e508c132dfc4bb36b71a4f5
Signed-off-by: Douglas Viroel <viroel@gmail.com>
This commit is contained in:
Douglas Viroel 2020-12-04 15:10:05 +00:00
parent 0d8f415e86
commit 8943e57ee6
4 changed files with 287 additions and 48 deletions

View File

@ -31,6 +31,7 @@ from manila.i18n import _
from manila.share.drivers.netapp.dataontap.client import api as netapp_api
from manila.share.drivers.netapp.dataontap.client import client_base
from manila.share.drivers.netapp import utils as na_utils
from manila import utils as manila_utils
LOG = log.getLogger(__name__)
@ -70,6 +71,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
ontapi_1_2x = (1, 20) <= ontapi_version < (1, 30)
ontapi_1_30 = ontapi_version >= (1, 30)
ontapi_1_110 = ontapi_version >= (1, 110)
ontapi_1_120 = ontapi_version >= (1, 120)
ontapi_1_140 = ontapi_version >= (1, 140)
ontapi_1_150 = ontapi_version >= (1, 150)
@ -91,6 +93,8 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
supported=ontapi_1_140)
self.features.add_feature('CIFS_DC_ADD_SKIP_CHECK',
supported=ontapi_1_150)
self.features.add_feature('LDAP_LDAP_SERVERS',
supported=ontapi_1_120)
def _invoke_vserver_api(self, na_element, vserver):
server = copy.copy(self.connection)
@ -1415,7 +1419,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
@na_utils.trace
def setup_security_services(self, security_services, vserver_client,
vserver_name):
vserver_name, timeout=30):
api_args = {
'name-mapping-switch': [
{'nmswitch': 'ldap'},
@ -1431,7 +1435,8 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
for security_service in security_services:
if security_service['type'].lower() == 'ldap':
vserver_client.configure_ldap(security_service)
vserver_client.configure_ldap(security_service,
timeout=timeout)
elif security_service['type'].lower() == 'active_directory':
vserver_client.configure_active_directory(security_service,
@ -1500,22 +1505,97 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
self.send_request('export-rule-create', export_rule_create_args)
@na_utils.trace
def configure_ldap(self, security_service):
"""Configures LDAP on Vserver."""
def _create_ldap_client(self, security_service):
ad_domain = security_service.get('domain')
ldap_servers = security_service.get('server')
bind_dn = security_service.get('user')
ldap_schema = 'RFC-2307'
if ad_domain:
if ldap_servers:
msg = _("LDAP client cannot be configured with both 'server' "
"and 'domain' parameters. Use 'server' for Linux/Unix "
"LDAP servers or 'domain' for Active Directory LDAP "
"servers.")
LOG.exception(msg)
raise exception.NetAppException(msg)
# RFC2307bis, for MS Active Directory LDAP server
ldap_schema = 'MS-AD-BIS'
bind_dn = (security_service.get('user') + '@' + ad_domain)
else:
if not ldap_servers:
msg = _("LDAP client cannot be configured without 'server' "
"or 'domain' parameters. Use 'server' for Linux/Unix "
"LDAP servers or 'domain' for Active Directory LDAP "
"server.")
LOG.exception(msg)
raise exception.NetAppException(msg)
if security_service.get('dns_ip'):
self.configure_dns(security_service)
config_name = hashlib.md5(six.b(security_service['id'])).hexdigest()
api_args = {
'ldap-client-config': config_name,
'servers': {
'ip-address': security_service['server'],
},
'tcp-port': '389',
'schema': 'RFC-2307',
'bind-password': security_service['password'],
'schema': ldap_schema,
'bind-dn': bind_dn,
'bind-password': security_service.get('password'),
}
if security_service.get('ou'):
api_args['base-dn'] = security_service['ou']
if ad_domain:
# Active Directory LDAP server
api_args['ad-domain'] = ad_domain
else:
# Linux/Unix LDAP servers
if self.features.LDAP_LDAP_SERVERS:
servers_key, servers_key_type = 'ldap-servers', 'string'
else:
servers_key, servers_key_type = 'servers', 'ip-address'
api_args[servers_key] = []
for server in ldap_servers.split(','):
api_args[servers_key].append(
{servers_key_type: server.strip()})
self.send_request('ldap-client-create', api_args)
api_args = {'client-config': config_name, 'client-enabled': 'true'}
self.send_request('ldap-config-create', api_args)
@na_utils.trace
def _enable_ldap_client(self, client_config_name, timeout=30):
# ONTAP ldap query timeout is 3 seconds by default
interval = 3
retries = int(timeout / interval) or 1
api_args = {'client-config': client_config_name,
'client-enabled': 'true'}
@manila_utils.retry(exception.ShareBackendException, interval=interval,
retries=retries, backoff_rate=1)
def try_enable_ldap_client():
try:
self.send_request('ldap-config-create', api_args)
except netapp_api.NaApiError as e:
msg = _('Unable to enable ldap client configuration. Will '
'retry the operation. Error details: %s') % e.message
LOG.warning(msg)
raise exception.ShareBackendException(msg=msg)
try:
try_enable_ldap_client()
except exception.ShareBackendException:
msg = _("Unable to enable ldap client configuration %s. "
"Retries exhausted. Aborting.") % client_config_name
LOG.exception(msg)
raise exception.NetAppException(message=msg)
@na_utils.trace
def configure_ldap(self, security_service, timeout=30):
"""Configures LDAP on Vserver."""
config_name = hashlib.md5(six.b(security_service['id'])).hexdigest()
self._create_ldap_client(security_service)
self._enable_ldap_client(config_name, timeout=timeout)
@na_utils.trace
def configure_active_directory(self, security_service, vserver_name):
@ -1679,24 +1759,65 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
@na_utils.trace
def configure_dns(self, security_service):
"""Configure DNS address and servers for a vserver."""
api_args = {
'domains': {
'string': security_service['domain'],
},
'domains': [],
'name-servers': [],
'dns-state': 'enabled',
}
# NOTE(dviroel): Read the current dns configuration and merge with the
# new one. This scenario is expected when 2 security services provide
# a DNS configuration, like 'active_directory' and 'ldap'.
current_dns_config = self.get_dns_config()
domains = set(current_dns_config.get('domains', []))
dns_ips = set(current_dns_config.get('dns-ips', []))
domains.add(security_service['domain'])
for domain in domains:
api_args['domains'].append({'string': domain})
for dns_ip in security_service['dns_ip'].split(','):
api_args['name-servers'].append({'ip-address': dns_ip.strip()})
dns_ips.add(dns_ip.strip())
for dns_ip in dns_ips:
api_args['name-servers'].append({'ip-address': dns_ip})
try:
self.send_request('net-dns-create', api_args)
except netapp_api.NaApiError as e:
if e.code == netapp_api.EDUPLICATEENTRY:
LOG.error("DNS exists for Vserver.")
if current_dns_config:
self.send_request('net-dns-modify', api_args)
else:
msg = _("Failed to configure DNS. %s")
raise exception.NetAppException(msg % e.message)
self.send_request('net-dns-create', api_args)
except netapp_api.NaApiError as e:
msg = _("Failed to configure DNS. %s")
raise exception.NetAppException(msg % e.message)
@na_utils.trace
def get_dns_config(self):
"""Read DNS servers and domains currently configured in the vserver·"""
api_args = {}
try:
result = self.send_request('net-dns-get', api_args)
except netapp_api.NaApiError as e:
if e.code == netapp_api.EOBJECTNOTFOUND:
return {}
msg = _("Failed to retrieve DNS configuration. %s")
raise exception.NetAppException(msg % e.message)
dns_config = {}
attributes = result.get_child_by_name('attributes')
dns_info = attributes.get_child_by_name('net-dns-info')
dns_config['dns-state'] = dns_info.get_child_content(
'dns-state')
domains = dns_info.get_child_by_name(
'domains') or netapp_api.NaElement('None')
dns_config['domains'] = [domain.get_content()
for domain in domains.get_children()]
servers = dns_info.get_child_by_name(
'name-servers') or netapp_api.NaElement('None')
dns_config['dns-ips'] = [server.get_content()
for server in servers.get_children()]
return dns_config
@na_utils.trace
def set_preferred_dc(self, security_service):

View File

@ -462,11 +462,26 @@ CIFS_SECURITY_SERVICE = {
'server': '',
}
LDAP_SECURITY_SERVICE = {
LDAP_LINUX_SECURITY_SERVICE = {
'id': 'fake_id',
'type': 'ldap',
'user': 'fake_user',
'password': 'fake_password',
'server': 'fake_server',
'ou': 'fake_ou',
'dns_ip': None,
'domain': None
}
LDAP_AD_SECURITY_SERVICE = {
'id': 'fake_id',
'type': 'ldap',
'user': 'fake_user',
'password': 'fake_password',
'domain': 'fake_domain',
'ou': 'fake_ou',
'dns_ip': 'fake_dns_ip',
'server': None,
}
KERBEROS_SECURITY_SERVICE = {
@ -2727,6 +2742,30 @@ KERBEROS_CONFIG_GET_RESPONSE = etree.XML("""
'vserver_name': VSERVER_NAME,
})
DNS_CONFIG_GET_RESPONSE = etree.XML("""
<results status="passed">
<attributes>
<net-dns-info>
<attempts>1</attempts>
<dns-state>enabled</dns-state>
<domains>
<string>fake_domain.com</string>
</domains>
<is-tld-query-enabled>true</is-tld-query-enabled>
<name-servers>
<ip-address>fake_dns_1</ip-address>
<ip-address>fake_dns_2</ip-address>
</name-servers>
<require-packet-query-match>true</require-packet-query-match>
<require-source-address-match>true</require-source-address-match>
<timeout>2</timeout>
<vserver-name>%(vserver_name)s</vserver-name>
</net-dns-info>
</attributes>
</results>""" % {
'vserver_name': VSERVER_NAME,
})
FAKE_VOL_XML = """<volume-info>
<name>open123</name>
<state>online</state>

View File

@ -2326,7 +2326,7 @@ class NetAppClientCmodeTestCase(test.TestCase):
self.mock_object(self.client, 'send_request')
self.mock_object(self.vserver_client, 'configure_ldap')
self.client.setup_security_services([fake.LDAP_SECURITY_SERVICE],
self.client.setup_security_services([fake.LDAP_LINUX_SECURITY_SERVICE],
self.vserver_client,
fake.VSERVER_NAME)
@ -2344,7 +2344,7 @@ class NetAppClientCmodeTestCase(test.TestCase):
self.client.send_request.assert_has_calls([
mock.call('vserver-modify', vserver_modify_args)])
self.vserver_client.configure_ldap.assert_has_calls([
mock.call(fake.LDAP_SECURITY_SERVICE)])
mock.call(fake.LDAP_LINUX_SECURITY_SERVICE, timeout=30)])
def test_setup_security_services_active_directory(self):
@ -2509,22 +2509,38 @@ class NetAppClientCmodeTestCase(test.TestCase):
mock.call('export-rule-create', export_rule_create_args),
mock.call('export-rule-create', export_rule_create_args2)])
def test_configure_ldap(self):
@ddt.data(fake.LDAP_LINUX_SECURITY_SERVICE, fake.LDAP_AD_SECURITY_SERVICE)
def test_configure_ldap(self, sec_service):
self.client.features.add_feature('LDAP_LDAP_SERVERS')
self.mock_object(self.client, 'send_request')
self.mock_object(self.client, 'configure_dns')
self.client.configure_ldap(fake.LDAP_SECURITY_SERVICE)
self.client.configure_ldap(sec_service)
config_name = hashlib.md5(
six.b(fake.LDAP_SECURITY_SERVICE['id'])).hexdigest()
config_name = hashlib.md5(six.b(sec_service['id'])).hexdigest()
ldap_client_create_args = {
'ldap-client-config': config_name,
'servers': {'ip-address': fake.LDAP_SECURITY_SERVICE['server']},
'tcp-port': '389',
'schema': 'RFC-2307',
'bind-password': fake.LDAP_SECURITY_SERVICE['password']
'bind-password': sec_service['password'],
}
if sec_service.get('domain'):
ldap_client_create_args['schema'] = 'MS-AD-BIS'
ldap_client_create_args['bind-dn'] = (
sec_service['user'] + '@' + sec_service['domain'])
ldap_client_create_args['ad-domain'] = sec_service['domain']
else:
ldap_client_create_args['schema'] = 'RFC-2307'
ldap_client_create_args['bind-dn'] = sec_service['user']
ldap_client_create_args['ldap-servers'] = [{
'string': sec_service['server']
}]
if sec_service.get('ou'):
ldap_client_create_args['base-dn'] = sec_service['ou']
ldap_config_create_args = {
'client-config': config_name,
'client-enabled': 'true'
@ -2534,6 +2550,32 @@ class NetAppClientCmodeTestCase(test.TestCase):
mock.call('ldap-client-create', ldap_client_create_args),
mock.call('ldap-config-create', ldap_config_create_args)])
@ddt.data({'server': None, 'domain': None},
{'server': 'fake_server', 'domain': 'fake_domain'})
@ddt.unpack
def test_configure_ldap_invalid_parameters(self, server, domain):
fake_ldap_sec_service = copy.deepcopy(fake.LDAP_AD_SECURITY_SERVICE)
fake_ldap_sec_service['server'] = server
fake_ldap_sec_service['domain'] = domain
self.assertRaises(exception.NetAppException,
self.client.configure_ldap,
fake_ldap_sec_service)
def test__enable_ldap_client_timeout(self):
mock_warning_log = self.mock_object(client_cmode.LOG, 'warning')
na_api_error = netapp_api.NaApiError(code=netapp_api.EAPIERROR)
mock_send_request = self.mock_object(
self.client, 'send_request', mock.Mock(side_effect=na_api_error))
self.assertRaises(exception.NetAppException,
self.client._enable_ldap_client,
'fake_config_name',
timeout=6)
self.assertEqual(2, mock_send_request.call_count)
self.assertEqual(2, mock_warning_log.call_count)
def test_configure_active_directory(self):
self.mock_object(self.client, 'send_request')
@ -2767,11 +2809,13 @@ class NetAppClientCmodeTestCase(test.TestCase):
def test_configure_dns_for_active_directory(self):
self.mock_object(self.client, 'send_request')
self.mock_object(self.client, 'get_dns_config',
mock.Mock(return_value={}))
self.client.configure_dns(fake.CIFS_SECURITY_SERVICE)
net_dns_create_args = {
'domains': {'string': fake.CIFS_SECURITY_SERVICE['domain']},
'domains': [{'string': fake.CIFS_SECURITY_SERVICE['domain']}],
'name-servers': [{
'ip-address': fake.CIFS_SECURITY_SERVICE['dns_ip']
}],
@ -2784,29 +2828,26 @@ class NetAppClientCmodeTestCase(test.TestCase):
def test_configure_dns_multiple_dns_ip(self):
self.mock_object(self.client, 'send_request')
self.mock_object(self.client, 'get_dns_config',
mock.Mock(return_value={}))
mock_dns_ips = ['10.0.0.1', '10.0.0.2', '10.0.0.3']
security_service = fake.CIFS_SECURITY_SERVICE
security_service['dns_ip'] = ', '.join(mock_dns_ips)
self.client.configure_dns(security_service)
net_dns_create_args = {
'domains': {'string': security_service['domain']},
'dns-state': 'enabled',
'name-servers': [{'ip-address': dns_ip} for dns_ip in mock_dns_ips]
}
self.client.send_request.assert_has_calls([
mock.call('net-dns-create', net_dns_create_args)])
self.client.send_request.assert_called_once()
def test_configure_dns_for_kerberos(self):
self.mock_object(self.client, 'send_request')
self.mock_object(self.client, 'get_dns_config',
mock.Mock(return_value={}))
self.client.configure_dns(fake.KERBEROS_SECURITY_SERVICE)
net_dns_create_args = {
'domains': {'string': fake.KERBEROS_SECURITY_SERVICE['domain']},
'domains': [{'string': fake.KERBEROS_SECURITY_SERVICE['domain']}],
'name-servers': [{
'ip-address': fake.KERBEROS_SECURITY_SERVICE['dns_ip']
}],
@ -2817,15 +2858,19 @@ class NetAppClientCmodeTestCase(test.TestCase):
mock.call('net-dns-create', net_dns_create_args)])
def test_configure_dns_already_present(self):
self.mock_object(self.client,
'send_request',
self._mock_api_error(code=netapp_api.EDUPLICATEENTRY))
dns_config = {
'dns-state': 'enabled',
'domains': [fake.KERBEROS_SECURITY_SERVICE['domain']],
'dns-ips': [fake.KERBEROS_SECURITY_SERVICE['dns_ip']],
}
self.mock_object(self.client, 'get_dns_config',
mock.Mock(return_value=dns_config))
self.mock_object(self.client, 'send_request')
self.client.configure_dns(fake.KERBEROS_SECURITY_SERVICE)
net_dns_create_args = {
'domains': {'string': fake.KERBEROS_SECURITY_SERVICE['domain']},
'domains': [{'string': fake.KERBEROS_SECURITY_SERVICE['domain']}],
'name-servers': [{
'ip-address': fake.KERBEROS_SECURITY_SERVICE['dns_ip']
}],
@ -2833,17 +2878,34 @@ class NetAppClientCmodeTestCase(test.TestCase):
}
self.client.send_request.assert_has_calls([
mock.call('net-dns-create', net_dns_create_args)])
self.assertEqual(1, client_cmode.LOG.error.call_count)
mock.call('net-dns-modify', net_dns_create_args)])
def test_configure_dns_api_error(self):
self.mock_object(self.client, 'send_request', self._mock_api_error())
self.mock_object(self.client, 'get_dns_config',
mock.Mock(return_value={}))
self.assertRaises(exception.NetAppException,
self.client.configure_dns,
fake.KERBEROS_SECURITY_SERVICE)
def test_get_dns_configuration(self):
api_response = netapp_api.NaElement(
fake.DNS_CONFIG_GET_RESPONSE)
self.mock_object(self.client, 'send_request',
mock.Mock(return_value=api_response))
result = self.client.get_dns_config()
expected_result = {
'dns-state': 'enabled',
'domains': ['fake_domain.com'],
'dns-ips': ['fake_dns_1', 'fake_dns_2']
}
self.assertEqual(expected_result, result)
self.client.send_request.assert_called_once_with('net-dns-get', {})
@ddt.data(
{
'server': '',

View File

@ -0,0 +1,17 @@
---
fixes:
- |
NetApp ONTAP driver is now fixed to properly configure SVM LDAP client when
configuration is provided through `ldap` security service. Now, the driver
chooses the correct LDAP schema based on the given security service
parameters. The `RFC-2307` schema will be set for Linux/Unix LDAP servers
and `RFC-2307bis` for Active Directory servers. When using a Linux/Unix
LDAP server, the security service should be configured setting the
`server` parameter with servers IPs or host names. For Active
Directory LDAP server, the domain information must be configured using the
the `domain` parameter. Users should provide at least one DNS server when
configuring servers by its host or domain names. The base search
`distinguished name` used for LDAP queries can now be configured using
security service `ou` parameter. Please refer to
`Launchpad Bug #1916534 <https://bugs.launchpad.net/manila/+bug/1916534>`_
for more details.