Merge pull request #2263 from KrisSaxton/develop

New aggregrated list support in pillar_ldap; code now pep-8 compliant.
This commit is contained in:
Thomas S Hatch 2012-10-17 08:30:52 -07:00
commit 13611ebdc1
2 changed files with 110 additions and 132 deletions

View File

@ -1,28 +1,33 @@
'''
Module to provide LDAP commands via salt.
This module was written by Kris Saxton <kris@automationlogic.com>
REQUIREMENT 1:
In order to connect to LDAP, certain configuration is required
in the salt minion config on the LDAP server.
The minimum configuration items that must be set are::
in the minion config on the LDAP server.
The minimum configuration items that must be set are:
ldap.basedn: dc=acme,dc=com (example values, adjust to suit)
If your LDAP server requires authentication then you must also set::
If your LDAP server requires authentication then you must also set:
ldap.binddn: <user>
ldap.bindpw: <password>
ldap.binddn: admin
ldap.bindpw: password
In addition, the following optional values may be set::
In addition, the following optional values may be set:
ldap.server: localhost (default=localhost)
ldap.port: 389 (default=389, standard port)
ldap.tls: False (default=False, no TLS)
ldap.scope: 2 (default=2, ldap.SCOPE_SUBTREE)
ldap.attrs: [saltAttr] (default=None, return all attributes)
ldap.server: localhost (default=localhost, see warning below)
ldap.port: 389 (default=389, standard port)
ldap.tls: False (default=False, no TLS)
ldap.scope: 2 (default=2, ldap.SCOPE_SUBTREE)
ldap.attrs: [saltAttr] (default=None, return all attributes)
WARNING:
At the moment this module only recommends connection to LDAP services
listening on 'localhost'. This is deliberate to avoid the potentially
dangerous situation of multiple minions sending identical update commands to
the same LDAP server. It's easy enough to override this behaviour,
but badness may ensue - you have been warned.
REQUIREMENT 2:
@ -46,15 +51,14 @@ except ImportError:
log = logging.getLogger(__name__)
# Defaults in the event that these are not found in the minion or pillar config
__opts__ = {
'ldap.server': 'localhost',
__opts__ = {'ldap.server': 'localhost',
'ldap.port': '389',
'ldap.tls': False,
'ldap.scope': 2,
'ldap.attrs': None,
'ldap.binddn': '',
'ldap.bindpw': ''
}
'ldap.bindpw': ''}
def __virtual__():
'''
@ -65,6 +69,7 @@ def __virtual__():
return 'ldap'
return False
def _config(name, key=None, **kwargs):
'''
Return a value for 'name' from command line args then config file options.
@ -78,10 +83,11 @@ def _config(name, key=None, **kwargs):
try:
value = __opts__['ldap.{0}'.format(key)]
except KeyError:
msg = 'missing ldap.{0} in config or {1} in args'.format(key, name)
raise SaltInvocationError(msg)
msg = 'missing ldap.{0} in config or {1} in args'.format(key, name)
raise SaltInvocationError(msg)
return value
def _connect(**kwargs):
'''
Instantiate LDAP Connection class and return an LDAP connection object
@ -90,44 +96,42 @@ def _connect(**kwargs):
for name in ['server', 'port', 'tls', 'binddn', 'bindpw']:
connargs[name] = _config(name, **kwargs)
ldap = _LDAPConnection(**connargs).LDAP
return ldap
return _LDAPConnection(**connargs).LDAP
def search(filter, dn=None, scope=None, attrs=None, **kwargs):
'''
Run an LDAP query and return the results.
Run an arbitrary LDAP query and return the results.
CLI Examples::
salt 'ldaphost' ldap.search filter=cn=myhost
returns::
'myhost': { 'count': 1,
salt 'ldaphost' ldap.search "filter=cn=myhost"
returns:
'myhost': { 'count': 1,
'results': [['cn=myhost,ou=hosts,o=acme,c=gb',
{'saltKeyValue': ['ntpserver=ntp.acme.local', 'foo=myfoo'],
'saltState': ['foo', 'bar']}]],
'time': {'human': '1.2ms', 'raw': '0.00123'}}}
Search and connection options can be overridden by specifying the relevant
option as key=value pairs, for example::
option as key=value pairs, for example:
salt 'ldaphost' ldap.search filter=cn=myhost dn=ou=hosts,o=acme,c=gb
scope=1 attrs='' server='localhost' port='7393' tls=True bindpw='ssh'
salt 'ldaphost' ldap.search filter=cn=myhost dn=ou=hosts,o=acme,c=gb scope=1 attrs='' server=localhost port=7393 tls=True bindpw=ssh
'''
if not dn:
dn = _config('dn', 'basedn')
if not scope:
scope = _config('scope')
if attrs == '': # Allows command line 'return all attributes' override
if attrs == '': # Allow command line 'return all' attr override
attrs = None
elif attrs == None:
elif attrs is None:
attrs = _config('attrs')
ldap = _connect(**kwargs)
_ldap = _connect(**kwargs)
start = time.time()
msg = 'Running LDAP search with filter:%s, dn:%s, scope:%s, attrs:%s' %\
(filter, dn, scope, attrs)
log.debug(msg)
results = ldap.search_s(dn, int(scope), filter, attrs)
results = _ldap.search_s(dn, int(scope), filter, attrs)
elapsed = (time.time() - start)
if elapsed < 0.200:
elapsed_h = str(round(elapsed * 1000, 1)) + 'ms'
@ -139,10 +143,11 @@ def search(filter, dn=None, scope=None, attrs=None, **kwargs):
ret['results'] = results
return ret
class _LDAPConnection:
"""Setup an LDAP connection."""
def __init__(self, server, port, tls, binddn, bindpw):
'''
Bind to an LDAP directory using passed credentials."""
@ -154,8 +159,8 @@ class _LDAPConnection:
self.bindpw = bindpw
try:
self.LDAP = ldap.initialize('ldap://%s:%s' %
(self.server, self.port))
self.LDAP.protocol_version = 3 #ldap.VERSION3
(self.server, self.port))
self.LDAP.protocol_version = 3 # ldap.VERSION3
if self.tls:
self.LDAP.start_tls_s()
self.LDAP.simple_bind_s(self.binddn, self.bindpw)

View File

@ -1,59 +1,8 @@
'''
Pillar LDAP is a plugin module for the salt pillar system which allows external
data (in this case data stored in an LDAP directory) to be incorporated into
salt state files.
This module was written by Kris Saxton <kris@automationlogic.com>
REQUIREMENTS:
The salt ldap module
An LDAP directory
INSTALLATION:
Drop this module into the 'pillar' directory under the root of the salt
python pkg; Restart your master.
CONFIGURATION:
Add something like the following to your salt master's config file:
ext_pillar:
- pillar_ldap: /etc/salt/pillar/plugins/pillar_ldap.yaml
Configure the 'pillar_ldap' config file with your LDAP sources
and an order in which to search them:
ldap: &defaults
server: localhost
port: 389
tls: False
dn: o=acme,c=gb
binddn: uid=admin,o=acme,c=gb
bindpw: sssssh
attrs: [saltKeyValue, saltState]
scope: 1
hosts:
<<: *defaults
filter: ou=hosts
dn: o=customer,o=acme,c=gb
{{ fqdn }}:
<<: *defaults
filter: cn={{ fqdn }}
dn: ou=hosts,o=customer,o=acme,c=gb
search_order:
- hosts
- {{ fqdn }}
Essentially whatever is referenced in the 'search_order' list will be searched
from first to last. The config file is templated allowing you to ref grains.
Where repeated instances of the same data are found during the searches, the
instance found latest in the search order will override any earlier instances.
This pillar module parses a config file (specified in the salt master config),
and executes a series of LDAP searches based on that config. Data returned by
these searches is aggregrated, with data items found later in the LDAP search
order overriding data found earlier on.
The final result set is merged with the pillar data.
'''
@ -62,11 +11,6 @@ import os
import logging
import traceback
# Import salt libs
import salt.config
import salt.utils
from salt._compat import string_types
# Import third party libs
import yaml
from jinja2 import Environment, FileSystemLoader
@ -80,6 +24,7 @@ except ImportError:
# Set up logging
log = logging.getLogger(__name__)
def __virtual__():
'''
Only return if ldap module is installed
@ -89,6 +34,7 @@ def __virtual__():
else:
return False
def _render_template(config_file):
'''
Render config template, substituting grains where found.
@ -99,6 +45,7 @@ def _render_template(config_file):
config = template.render(__grains__)
return config
def _config(name, conf):
'''
Return a value for 'name' from the config file options.
@ -109,35 +56,53 @@ def _config(name, conf):
value = None
return value
def _result_to_dict(data, attrs=None):
def _result_to_dict(data, result, conf):
'''
Formats LDAP search results as a pillar dictionary.
Attributes tagged in the pillar config file ('attrs') are scannned for the
'key=value' format. Matches are written to the dictionary directly as:
dict[key] = value
Aggregates LDAP search result based on rules, returns a dictionary.
Rules:
Attributes tagged in the pillar config as 'attrs' or 'lists' are
scanned for a 'key=value' format (non matching entires are ignored.
Entries matching the 'attrs' tag overwrite previous values where
the key matches a previous result.
Entries matching the 'lists' tag are appended to list of values where
the key matches a previous result.
All Matching entries are then written directly to the pillar data
dictionary as data[key] = value.
For example, search result:
saltKeyValue': ['ntpserver=ntp.acme.local', 'foo=myfoo']
{ saltKeyValue': ['ntpserver=ntp.acme.local', 'foo=myfoo'],
'saltList': ['vhost=www.acme.net', 'vhost=www.acme.local' }
is written to the pillar data dictionary as:
{'ntpserver': 'ntp.acme.local', 'foo': 'myfoo'}
{ 'ntpserver': 'ntp.acme.local', 'foo': 'myfoo',
'vhost': ['www.acme.net', 'www.acme.local' }
'''
if not attrs:
attrs = []
result = {}
for key in data:
attrs = _config('attrs', conf) or []
lists = _config('lists', conf) or []
for key in result:
if key in attrs:
for item in data.get(key):
for item in result.get(key):
if '=' in item:
k, v = item.split('=')
result[k] = v
else:
result[key] = data.get(key)
else:
result[key] = data.get(key)
return result
k, v = item.split('=', 1)
data[k] = v
elif key in lists:
for item in result.get(key):
if '=' in item:
k, v = item.split('=', 1)
if not k in data:
data[k] = [v]
else:
data[k].append(v)
print 'Returning data %s' % data
return data
def _do_search(conf):
'''
@ -154,30 +119,37 @@ def _do_search(conf):
except KeyError:
raise SaltInvocationError('missing filter')
dn = _config('dn', conf)
scope = _config('scope', conf)
attrs = _config('attrs', conf)
scope = _config('scope', conf)
_lists = _config('lists', conf) or []
_attrs = _config('attrs', conf) or []
attrs = _lists + _attrs
if not attrs:
attrs = None
# Perform the search
try:
raw_result = __salt__['ldap.search'](filter, dn, scope, attrs, **connargs)['results'][0][1]
except IndexError: # we got no results for this search
raw_result = {}
log.debug('LDAP search returned no results for filter {0}'.format(filter))
result = __salt__['ldap.search'](filter, dn, scope, attrs,
**connargs)['results'][0][1]
msg = 'LDAP search returned no results for filter {0}'.format(filter)
log.debug(msg)
except IndexError: # we got no results for this search
result = {}
except Exception:
msg = traceback.format_exc()
log.critical('Failed to retrieve pillar data from LDAP: {0}'.format(msg))
trace = traceback.format_exc()
msg = 'Failed to retrieve pillar data from LDAP: {0}'.format(trace)
log.critical(msg)
return {}
result = _result_to_dict(raw_result, attrs)
return result
def ext_pillar(config_file):
'''
Execute LDAP searches and return the aggregated data
'''
if os.path.isfile(config_file):
try:
with open(config_file, 'r') as raw_config:
config = _render_template(config_file) or {}
opts = yaml.safe_load(config) or {}
#open(config_file, 'r') as raw_config:
config = _render_template(config_file) or {}
opts = yaml.safe_load(config) or {}
opts['conf_file'] = config_file
except Exception as e:
import salt.log
@ -193,6 +165,7 @@ def ext_pillar(config_file):
for source in opts['search_order']:
config = opts[source]
result = _do_search(config)
print 'source %s got result %s' % (source, result)
if result:
data.update(result)
data = _result_to_dict(data, result, config)
return data