mirror of
https://github.com/valitydev/salt.git
synced 2024-11-07 17:09:03 +00:00
Merge pull request #2263 from KrisSaxton/develop
New aggregrated list support in pillar_ldap; code now pep-8 compliant.
This commit is contained in:
commit
13611ebdc1
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user