mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Merge branch 'develop' of github.com:saltstack/salt into develop
This commit is contained in:
commit
b2a0b6e5fb
22 changed files with 637 additions and 150 deletions
|
@ -151,9 +151,7 @@
|
|||
# external_auth:
|
||||
# pam:
|
||||
# fred:
|
||||
# - keys: ro
|
||||
# - file_roots: ro
|
||||
# - 'G@os:RedHat': test.ping
|
||||
# - test.*
|
||||
|
||||
##### Master Module Management #####
|
||||
##########################################
|
||||
|
|
2
debian/changelog
vendored
2
debian/changelog
vendored
|
@ -2,7 +2,7 @@ salt (0.10.3) precise; urgency=low
|
|||
|
||||
* New upstream version
|
||||
|
||||
-- Thomas S Hatch <thatch@saltstack.com> Sun, 30 Aug 2012 13:34:10 -0700
|
||||
-- Tom Vaughan <thomas.david.vaughan@gmail.com> Sun, 30 Aug 2012 13:34:10 -0700
|
||||
|
||||
salt (0.10.2) precise; urgency=low
|
||||
|
||||
|
|
2
debian/copyright
vendored
2
debian/copyright
vendored
|
@ -1,7 +1,7 @@
|
|||
Format: http://dep.debian.net/deps/dep5
|
||||
Upstream-Name: salt
|
||||
Upstream-Contact: salt-users@googlegroups.com
|
||||
Source: https://github.com/downloads/saltstack/salt/salt-0.9.9.tar.gz
|
||||
Source: https://github.com/downloads/saltstack/salt/salt-0.10.3.tar.gz
|
||||
|
||||
Files: *
|
||||
Copyright: 2012 Thomas S Hatch <thatch45@gmail.com>
|
||||
|
|
3
debian/salt-master.upstart
vendored
3
debian/salt-master.upstart
vendored
|
@ -5,7 +5,4 @@ start on (net-device-up
|
|||
and runlevel [2345])
|
||||
stop on runlevel [!2345]
|
||||
|
||||
respawn limit 10 5
|
||||
respawn
|
||||
|
||||
exec /usr/bin/salt-master >/dev/null 2>&1
|
||||
|
|
3
debian/salt-minion.upstart
vendored
3
debian/salt-minion.upstart
vendored
|
@ -5,7 +5,4 @@ start on (net-device-up
|
|||
and runlevel [2345])
|
||||
stop on runlevel [!2345]
|
||||
|
||||
respawn limit 10 5
|
||||
respawn
|
||||
|
||||
exec /usr/bin/salt-minion >/dev/null 2>&1
|
||||
|
|
|
@ -94,7 +94,11 @@ Options
|
|||
.. option:: -N, --nodegroup
|
||||
|
||||
Use a predefined compound target defined in the Salt master configuration
|
||||
file
|
||||
file.
|
||||
|
||||
.. option:: -S, --ipcidr
|
||||
|
||||
Match based on Subnet (CIDR notation) or IPv4 address.
|
||||
|
||||
.. option:: -R, --range
|
||||
|
||||
|
|
|
@ -36,5 +36,5 @@ existing user keys and re-start the Salt master:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
rm /var/cache/salt/.*keys
|
||||
rm /var/cache/salt/.*key
|
||||
service salt-master restart
|
||||
|
|
|
@ -21,6 +21,7 @@ E PCRE Minion id match ``E@web\d+\.(dev|qa|prod)\.loc``
|
|||
P Grains PCRE match ``P@os:(RedHat|Fedora|CentOS)``
|
||||
L List of minions ``L@minion1.example.com,minion3.domain.com and bl*.domain.com``
|
||||
I Pillar glob match ``I@pdata:foobar``
|
||||
S Subnet/IP addr match ``S@192.168.1.0/24`` or ``S@192.168.1.100``
|
||||
====== ==================== ===============================================================
|
||||
|
||||
Matchers can be joined using boolean ``and``, ``or``, and ``not`` operators.
|
||||
|
|
|
@ -35,6 +35,22 @@ class LoadAuth(object):
|
|||
self.serial = salt.payload.Serial(opts)
|
||||
self.auth = salt.loader.auth(opts)
|
||||
|
||||
def load_name(self, load):
|
||||
'''
|
||||
Return the primary name associate with the load, if an empty string
|
||||
is returned then the load does not match the function
|
||||
'''
|
||||
if not 'fun' in load:
|
||||
return ''
|
||||
fstr = '{0}.auth'.format(load['fun'])
|
||||
if not fstr in self.auth:
|
||||
return ''
|
||||
fcall = salt.utils.format_call(self.auth[fstr], load)
|
||||
try:
|
||||
return fcall['args'][0]
|
||||
except IndexError:
|
||||
return ''
|
||||
|
||||
def auth_call(self, load):
|
||||
'''
|
||||
Return the token and set the cache data for use
|
||||
|
|
209
salt/client.py
209
salt/client.py
|
@ -258,6 +258,54 @@ class LocalClient(object):
|
|||
2,
|
||||
tgt_type)
|
||||
|
||||
def _check_pub_data(self, pub_data):
|
||||
'''
|
||||
Common checks on the pub_data data structure returned from running pub
|
||||
'''
|
||||
if not pub_data:
|
||||
err = ('Failed to authenticate, is this user permitted to execute '
|
||||
'commands?\n')
|
||||
sys.stderr.write(err)
|
||||
sys.exit(4)
|
||||
|
||||
# Failed to connect to the master and send the pub
|
||||
if not 'jid' in pub_data or pub_data['jid'] == '0':
|
||||
return {}
|
||||
|
||||
return pub_data
|
||||
|
||||
def run_job(self,
|
||||
tgt,
|
||||
fun,
|
||||
arg,
|
||||
expr_form,
|
||||
ret,
|
||||
timeout,
|
||||
**kwargs):
|
||||
'''
|
||||
Prep the job dir and send minions the pub.
|
||||
Returns a dict of (checked) pub_data or an empty dict.
|
||||
'''
|
||||
try:
|
||||
jid = salt.utils.prep_jid(
|
||||
self.opts['cachedir'],
|
||||
self.opts['hash_type']
|
||||
)
|
||||
except Exception:
|
||||
jid = ''
|
||||
|
||||
pub_data = self.pub(
|
||||
tgt,
|
||||
fun,
|
||||
arg,
|
||||
expr_form,
|
||||
ret,
|
||||
jid=jid,
|
||||
timeout=timeout or self.opts['timeout'],
|
||||
**kwargs)
|
||||
|
||||
return self._check_pub_data(pub_data)
|
||||
|
||||
def cmd(
|
||||
self,
|
||||
tgt,
|
||||
|
@ -266,39 +314,26 @@ class LocalClient(object):
|
|||
timeout=None,
|
||||
expr_form='glob',
|
||||
ret='',
|
||||
kwarg=None):
|
||||
kwarg=None,
|
||||
**kwargs):
|
||||
'''
|
||||
Execute a salt command and return.
|
||||
'''
|
||||
arg = condition_kwarg(arg, kwarg)
|
||||
if timeout is None:
|
||||
timeout = self.opts['timeout']
|
||||
try:
|
||||
jid = salt.utils.prep_jid(
|
||||
self.opts['cachedir'],
|
||||
self.opts['hash_type']
|
||||
)
|
||||
except Exception:
|
||||
jid = ''
|
||||
pub_data = self.pub(
|
||||
pub_data = self.run_job(
|
||||
tgt,
|
||||
fun,
|
||||
arg,
|
||||
expr_form,
|
||||
ret,
|
||||
jid=jid,
|
||||
timeout=timeout)
|
||||
timeout,
|
||||
**kwargs)
|
||||
|
||||
if not pub_data:
|
||||
err = ('Failed to authenticate, is this user permitted to execute '
|
||||
'commands?\n')
|
||||
sys.stderr.write(err)
|
||||
sys.exit(4)
|
||||
if pub_data['jid'] == '0':
|
||||
# Failed to connect to the master and send the pub
|
||||
return {}
|
||||
elif not pub_data['jid']:
|
||||
return {}
|
||||
return self.get_returns(pub_data['jid'], pub_data['minions'], timeout)
|
||||
return pub_data
|
||||
|
||||
return self.get_returns(pub_data['jid'], pub_data['minions'],
|
||||
timeout or self.opts['timeout'])
|
||||
|
||||
def cmd_cli(
|
||||
self,
|
||||
|
@ -309,49 +344,35 @@ class LocalClient(object):
|
|||
expr_form='glob',
|
||||
ret='',
|
||||
verbose=False,
|
||||
kwarg=None):
|
||||
kwarg=None,
|
||||
**kwargs):
|
||||
'''
|
||||
Execute a salt command and return data conditioned for command line
|
||||
output
|
||||
'''
|
||||
arg = condition_kwarg(arg, kwarg)
|
||||
if timeout is None:
|
||||
timeout = self.opts['timeout']
|
||||
try:
|
||||
jid = salt.utils.prep_jid(
|
||||
self.opts['cachedir'],
|
||||
self.opts['hash_type']
|
||||
)
|
||||
except Exception:
|
||||
jid = ''
|
||||
pub_data = self.pub(
|
||||
pub_data = self.run_job(
|
||||
tgt,
|
||||
fun,
|
||||
arg,
|
||||
expr_form,
|
||||
ret,
|
||||
jid=jid,
|
||||
timeout=timeout)
|
||||
timeout,
|
||||
**kwargs)
|
||||
|
||||
if not pub_data:
|
||||
err = ('Failed to authenticate, is this user permitted to execute '
|
||||
'commands?\n')
|
||||
sys.stderr.write(err)
|
||||
sys.exit(4)
|
||||
if pub_data['jid'] == '0':
|
||||
print('Failed to connect to the Master, is the Salt Master running?')
|
||||
yield {}
|
||||
elif not pub_data['jid']:
|
||||
print('No minions match the target')
|
||||
yield {}
|
||||
yield pub_data
|
||||
else:
|
||||
for fn_ret in self.get_cli_event_returns(pub_data['jid'],
|
||||
pub_data['minions'],
|
||||
timeout,
|
||||
timeout or self.opts['timeout'],
|
||||
tgt,
|
||||
expr_form,
|
||||
verbose):
|
||||
|
||||
if not fn_ret:
|
||||
continue
|
||||
|
||||
yield fn_ret
|
||||
|
||||
def cmd_iter(
|
||||
|
@ -362,36 +383,24 @@ class LocalClient(object):
|
|||
timeout=None,
|
||||
expr_form='glob',
|
||||
ret='',
|
||||
kwarg=None):
|
||||
kwarg=None,
|
||||
**kwargs):
|
||||
'''
|
||||
Execute a salt command and return an iterator to return data as it is
|
||||
received
|
||||
'''
|
||||
arg = condition_kwarg(arg, kwarg)
|
||||
if timeout is None:
|
||||
timeout = self.opts['timeout']
|
||||
jid = salt.utils.prep_jid(
|
||||
self.opts['cachedir'],
|
||||
self.opts['hash_type']
|
||||
)
|
||||
pub_data = self.pub(
|
||||
pub_data = self.run_job(
|
||||
tgt,
|
||||
fun,
|
||||
arg,
|
||||
expr_form,
|
||||
ret,
|
||||
jid=jid,
|
||||
timeout=timeout)
|
||||
timeout,
|
||||
**kwargs)
|
||||
|
||||
if not pub_data:
|
||||
err = ('Failed to authenticate, is this user permitted to execute '
|
||||
'commands?\n')
|
||||
sys.stderr.write(err)
|
||||
sys.exit(4)
|
||||
if pub_data['jid'] == '0':
|
||||
# Failed to connect to the master and send the pub
|
||||
yield {}
|
||||
elif not pub_data['jid']:
|
||||
yield {}
|
||||
yield pub_data
|
||||
else:
|
||||
for fn_ret in self.get_iter_returns(pub_data['jid'],
|
||||
pub_data['minions'],
|
||||
|
@ -408,35 +417,23 @@ class LocalClient(object):
|
|||
timeout=None,
|
||||
expr_form='glob',
|
||||
ret='',
|
||||
kwarg=None):
|
||||
kwarg=None,
|
||||
**kwargs):
|
||||
'''
|
||||
Execute a salt command and return
|
||||
'''
|
||||
arg = condition_kwarg(arg, kwarg)
|
||||
if timeout is None:
|
||||
timeout = self.opts['timeout']
|
||||
jid = salt.utils.prep_jid(
|
||||
self.opts['cachedir'],
|
||||
self.opts['hash_type']
|
||||
)
|
||||
pub_data = self.pub(
|
||||
pub_data = self.run_job(
|
||||
tgt,
|
||||
fun,
|
||||
arg,
|
||||
expr_form,
|
||||
ret,
|
||||
jid=jid,
|
||||
timeout=timeout)
|
||||
timeout,
|
||||
**kwargs)
|
||||
|
||||
if not pub_data:
|
||||
err = ('Failed to authenticate, is this user permitted to execute '
|
||||
'commands?\n')
|
||||
sys.stderr.write(err)
|
||||
sys.exit(4)
|
||||
if pub_data['jid'] == '0':
|
||||
# Failed to connect to the master and send the pub
|
||||
yield {}
|
||||
elif not pub_data['jid']:
|
||||
yield {}
|
||||
yield pub_data
|
||||
else:
|
||||
for fn_ret in self.get_iter_returns(pub_data['jid'],
|
||||
pub_data['minions'],
|
||||
|
@ -452,35 +449,24 @@ class LocalClient(object):
|
|||
expr_form='glob',
|
||||
ret='',
|
||||
verbose=False,
|
||||
kwarg=None):
|
||||
kwarg=None,
|
||||
**kwargs):
|
||||
'''
|
||||
Execute a salt command and return
|
||||
'''
|
||||
arg = condition_kwarg(arg, kwarg)
|
||||
if timeout is None:
|
||||
timeout = self.opts['timeout']
|
||||
jid = salt.utils.prep_jid(
|
||||
self.opts['cachedir'],
|
||||
self.opts['hash_type']
|
||||
)
|
||||
pub_data = self.pub(
|
||||
pub_data = self.run_job(
|
||||
tgt,
|
||||
fun,
|
||||
arg,
|
||||
expr_form,
|
||||
ret,
|
||||
jid=jid,
|
||||
timeout=timeout)
|
||||
timeout,
|
||||
**kwargs)
|
||||
|
||||
if not pub_data:
|
||||
err = ('Failed to authenticate, is this user permitted to execute '
|
||||
'commands?\n')
|
||||
sys.stderr.write(err)
|
||||
sys.exit(4)
|
||||
if pub_data['jid'] == '0':
|
||||
# Failed to connect to the master and send the pub
|
||||
return {}
|
||||
elif not pub_data['jid']:
|
||||
return {}
|
||||
return pub_data
|
||||
|
||||
return (self.get_cli_static_event_returns(pub_data['jid'],
|
||||
pub_data['minions'],
|
||||
timeout,
|
||||
|
@ -968,8 +954,15 @@ class LocalClient(object):
|
|||
minions = expr
|
||||
return minions
|
||||
|
||||
def pub(self, tgt, fun, arg=(), expr_form='glob',
|
||||
ret='', jid='', timeout=5):
|
||||
def pub(self,
|
||||
tgt,
|
||||
fun,
|
||||
arg=(),
|
||||
expr_form='glob',
|
||||
ret='',
|
||||
jid='',
|
||||
timeout=5,
|
||||
**kwargs):
|
||||
'''
|
||||
Take the required arguments and publish the given command.
|
||||
Arguments:
|
||||
|
@ -1037,6 +1030,10 @@ class LocalClient(object):
|
|||
'ret': ret,
|
||||
'jid': jid}
|
||||
|
||||
# if kwargs are passed, pack them.
|
||||
if kwargs:
|
||||
payload_kwargs['kwargs'] = kwargs
|
||||
|
||||
# If we have a salt user, add it to the payload
|
||||
if self.salt_user:
|
||||
payload_kwargs['user'] = self.salt_user
|
||||
|
|
|
@ -275,6 +275,7 @@ def master_config(path):
|
|||
'syndic_master': '',
|
||||
'runner_dirs': [],
|
||||
'client_acl': {},
|
||||
'external_auth': {},
|
||||
'file_buffer_size': 1048576,
|
||||
'max_open_files': 100000,
|
||||
'hash_type': 'md5',
|
||||
|
|
|
@ -1371,11 +1371,25 @@ class ClearFuncs(object):
|
|||
This method sends out publications to the minions, it can only be used
|
||||
by the LocalClient.
|
||||
'''
|
||||
extra = clear_load.get('extra', {})
|
||||
# Check for external auth calls
|
||||
if 'eauth' in clear_load:
|
||||
pass
|
||||
if 'eauth' in extra:
|
||||
if not extra['eauth'] in self.opts['external_auth']:
|
||||
# The eauth system is not enabled, fail
|
||||
return ''
|
||||
name = self.auth.load_name(extra)
|
||||
if not name in self.opts['external_auth'][extra['eauth']]:
|
||||
return ''
|
||||
if not self.auth.time_auth(extra):
|
||||
return ''
|
||||
good = False
|
||||
for regex in self.opts['external_auth'][extra['eauth']][name]:
|
||||
if re.match(regex, extra['fun']):
|
||||
good = True
|
||||
if not good:
|
||||
return ''
|
||||
# Verify that the caller has root on master
|
||||
if 'user' in clear_load:
|
||||
elif 'user' in clear_load:
|
||||
if clear_load['user'].startswith('sudo_'):
|
||||
if not clear_load.pop('key') == self.key.get(getpass.getuser(), ''):
|
||||
return ''
|
||||
|
|
|
@ -857,6 +857,25 @@ class Matcher(object):
|
|||
comps[1].lower(),
|
||||
))
|
||||
|
||||
def ipcidr_match(self, tgt):
|
||||
'''
|
||||
Matches based on ip address or CIDR notation
|
||||
'''
|
||||
num_parts = len(tgt.split('/'))
|
||||
if num_parts > 2:
|
||||
return False
|
||||
elif num_parts == 2:
|
||||
return self.functions['network.in_subnet'](tgt)
|
||||
else:
|
||||
import socket
|
||||
try:
|
||||
socket.inet_aton(tgt)
|
||||
except socket.error:
|
||||
# Not a valid IPv4 address
|
||||
return False
|
||||
else:
|
||||
return tgt in self.functions['network.ip_addrs']()
|
||||
|
||||
def compound_match(self, tgt):
|
||||
'''
|
||||
Runs the compound target check
|
||||
|
@ -869,12 +888,13 @@ class Matcher(object):
|
|||
'X': 'exsel',
|
||||
'I': 'pillar',
|
||||
'L': 'list',
|
||||
'S': 'ipcidr',
|
||||
'E': 'pcre'}
|
||||
results = []
|
||||
opers = ['and', 'or', 'not']
|
||||
for match in tgt.split():
|
||||
# Try to match tokens from the compound target, first by using
|
||||
# the 'G, X, I, L, E' matcher types, then by hostname glob.
|
||||
# the 'G, X, I, L, S, E' matcher types, then by hostname glob.
|
||||
if '@' in match and match[1] == '@':
|
||||
comps = match.split('@')
|
||||
matcher = ref.get(comps[0])
|
||||
|
|
|
@ -55,11 +55,15 @@ def info(name):
|
|||
|
||||
salt '*' group.info foo
|
||||
'''
|
||||
grinfo = grp.getgrnam(name)
|
||||
return {'name': grinfo.gr_name,
|
||||
'passwd': grinfo.gr_passwd,
|
||||
'gid': grinfo.gr_gid,
|
||||
'members': grinfo.gr_mem}
|
||||
try:
|
||||
grinfo = grp.getgrnam(name)
|
||||
except KeyError:
|
||||
return {}
|
||||
else:
|
||||
return {'name': grinfo.gr_name,
|
||||
'passwd': grinfo.gr_passwd,
|
||||
'gid': grinfo.gr_gid,
|
||||
'members': grinfo.gr_mem}
|
||||
|
||||
|
||||
def getent():
|
||||
|
|
165
salt/modules/ldap.py
Normal file
165
salt/modules/ldap.py
Normal file
|
@ -0,0 +1,165 @@
|
|||
'''
|
||||
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:
|
||||
|
||||
ldap.basedn: dc=acme,dc=com (example values, adjust to suit)
|
||||
|
||||
If your LDAP server requires authentication then you must also set:
|
||||
|
||||
ldap.binddn: <user>
|
||||
ldap.bindpw: <password>
|
||||
|
||||
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)
|
||||
|
||||
REQUIREMENT 2:
|
||||
|
||||
Required python modules: ldap
|
||||
'''
|
||||
# Import Python libs
|
||||
import time
|
||||
import logging
|
||||
|
||||
# Import salt libs
|
||||
from salt.exceptions import CommandExecutionError, SaltInvocationError
|
||||
|
||||
# Import third party libs
|
||||
try:
|
||||
import ldap
|
||||
import ldap.modlist
|
||||
has_ldap = True
|
||||
except ImportError:
|
||||
has_ldap = False
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Defaults in the event that these are not found in the minion or pillar config
|
||||
__opts__ = {
|
||||
'ldap.server': 'localhost',
|
||||
'ldap.port': '389',
|
||||
'ldap.tls': False,
|
||||
'ldap.scope': 2,
|
||||
'ldap.attrs': None,
|
||||
'ldap.binddn': '',
|
||||
'ldap.bindpw': ''
|
||||
}
|
||||
|
||||
def __virtual__():
|
||||
'''
|
||||
Only load this module if the ldap config is set
|
||||
'''
|
||||
# These config items must be set in the minion config
|
||||
if has_ldap:
|
||||
return 'ldap'
|
||||
return False
|
||||
|
||||
def _config(name, key=None, **kwargs):
|
||||
'''
|
||||
Return a value for 'name' from command line args then config file options.
|
||||
Specify 'key' if the config file option is not the same as 'name'.
|
||||
'''
|
||||
if key is None:
|
||||
key = name
|
||||
if name in kwargs:
|
||||
value = kwargs[name]
|
||||
else:
|
||||
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)
|
||||
return value
|
||||
|
||||
def _connect(**kwargs):
|
||||
'''
|
||||
Instantiate LDAP Connection class and return an LDAP connection object
|
||||
'''
|
||||
connargs = {}
|
||||
for name in ['server', 'port', 'tls', 'binddn', 'bindpw']:
|
||||
connargs[name] = _config(name, **kwargs)
|
||||
|
||||
ldap = _LDAPConnection(**connargs).LDAP
|
||||
return ldap
|
||||
|
||||
def search(filter, dn=None, scope=None, attrs=None, **kwargs):
|
||||
'''
|
||||
Run an LDAP query and return the results.
|
||||
|
||||
CLI Examples:
|
||||
|
||||
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:
|
||||
|
||||
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
|
||||
attrs = None
|
||||
elif attrs == None:
|
||||
attrs = _config('attrs')
|
||||
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)
|
||||
elapsed = (time.time() - start)
|
||||
if elapsed < 0.200:
|
||||
elapsed_h = str(round(elapsed * 1000, 1)) + 'ms'
|
||||
else:
|
||||
elapsed_h = str(round(elapsed, 2)) + 's'
|
||||
ret = {}
|
||||
ret['time'] = {'human': elapsed_h, 'raw': str(round(elapsed, 5))}
|
||||
ret['count'] = len(results)
|
||||
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."""
|
||||
'''
|
||||
self.server = server
|
||||
self.port = port
|
||||
self.tls = tls
|
||||
self.binddn = binddn
|
||||
self.bindpw = bindpw
|
||||
try:
|
||||
self.LDAP = ldap.initialize('ldap://%s:%s' %
|
||||
(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)
|
||||
except Exception:
|
||||
msg = 'Failed to bind to LDAP server %s:%s as %s' % \
|
||||
(self.server, self.port, self.binddn)
|
||||
raise CommandExecutionError(msg)
|
|
@ -426,7 +426,7 @@ def user_exists(user, host='localhost'):
|
|||
db = connect()
|
||||
cur = db.cursor()
|
||||
query = ('SELECT User,Host FROM mysql.user WHERE User = \'{0}\' AND '
|
||||
'Host = \'{0}\''.format(user, host))
|
||||
'Host = \'{1}\''.format(user, host))
|
||||
log.debug('Doing query: {0}'.format(query))
|
||||
cur.execute(query)
|
||||
return cur.rowcount == 1
|
||||
|
|
|
@ -293,8 +293,6 @@ def in_subnet(cidr):
|
|||
log.error('Invalid CIDR \'{0}\''.format(cidr))
|
||||
return False
|
||||
|
||||
ifaces = interfaces()
|
||||
|
||||
netstart_bin = _ipv4_to_bits(netstart)
|
||||
|
||||
if netsize < 32 and len(netstart_bin.rstrip('0')) > netsize:
|
||||
|
@ -303,18 +301,41 @@ def in_subnet(cidr):
|
|||
return False
|
||||
|
||||
netstart_leftbits = netstart_bin[0:netsize]
|
||||
for ipv4_info in ifaces.values():
|
||||
for ipv4 in ipv4_info.get('inet', []):
|
||||
if ipv4['address'] == '127.0.0.1': continue
|
||||
if netsize == 32:
|
||||
if netstart == ipv4['address']: return True
|
||||
else:
|
||||
ip_leftbits = _ipv4_to_bits(ipv4['address'])[0:netsize]
|
||||
if netstart_leftbits == ip_leftbits: return True
|
||||
for ip_addr in ip_addrs():
|
||||
if netsize == 32:
|
||||
if netstart == ip_addr: return True
|
||||
else:
|
||||
ip_leftbits = _ipv4_to_bits(ip_addr)[0:netsize]
|
||||
if netstart_leftbits == ip_leftbits: return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def ip_addrs():
|
||||
'''
|
||||
Returns a list of IPv4 addresses assigned to the host. (127.0.0.1 is
|
||||
ignored)
|
||||
'''
|
||||
ret = []
|
||||
ifaces = interfaces()
|
||||
for ipv4_info in ifaces.values():
|
||||
for ipv4 in ipv4_info.get('inet',[]):
|
||||
if ipv4['address'] != '127.0.0.1': ret.append(ipv4['address'])
|
||||
return ret
|
||||
|
||||
|
||||
def ip_addrs6():
|
||||
'''
|
||||
Returns a list of IPv6 addresses assigned to the host. (::1 is ignored)
|
||||
'''
|
||||
ret = []
|
||||
ifaces = interfaces()
|
||||
for ipv6_info in ifaces.values():
|
||||
for ipv6 in ipv6_info.get('inet6',[]):
|
||||
if ipv6['address'] != '::1': ret.append(ipv6['address'])
|
||||
return ret
|
||||
|
||||
|
||||
def ping(host):
|
||||
'''
|
||||
Performs a ping to a host
|
||||
|
|
|
@ -53,11 +53,15 @@ def info(name):
|
|||
|
||||
salt '*' group.info foo
|
||||
'''
|
||||
grinfo = grp.getgrnam(name)
|
||||
return {'name': grinfo.gr_name,
|
||||
'passwd': grinfo.gr_passwd,
|
||||
'gid': grinfo.gr_gid,
|
||||
'members': grinfo.gr_mem}
|
||||
try:
|
||||
grinfo = grp.getgrnam(name)
|
||||
except KeyError:
|
||||
return {}
|
||||
else:
|
||||
return {'name': grinfo.gr_name,
|
||||
'passwd': grinfo.gr_passwd,
|
||||
'gid': grinfo.gr_gid,
|
||||
'members': grinfo.gr_mem}
|
||||
|
||||
|
||||
def getent():
|
||||
|
|
|
@ -327,7 +327,7 @@ class Pillar(object):
|
|||
else:
|
||||
ext.update(self.ext_pillars[key](val))
|
||||
except Exception:
|
||||
log.critical('Failed to load ext_pillar {0}'.format(key))
|
||||
log.exception('Failed to load ext_pillar {0}'.format(key))
|
||||
return ext
|
||||
|
||||
def compile_pillar(self):
|
||||
|
|
198
salt/pillar/pillar_ldap.py
Normal file
198
salt/pillar/pillar_ldap.py
Normal file
|
@ -0,0 +1,198 @@
|
|||
'''
|
||||
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.
|
||||
The final result set is merged with the pillar data.
|
||||
'''
|
||||
|
||||
# Import python libs
|
||||
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
|
||||
try:
|
||||
import ldap
|
||||
import ldap.modlist
|
||||
has_ldap = True
|
||||
except ImportError:
|
||||
has_ldap = False
|
||||
|
||||
# Set up logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def __virtual__():
|
||||
'''
|
||||
Only return if ldap module is installed
|
||||
'''
|
||||
if has_ldap:
|
||||
return 'pillar_ldap'
|
||||
else:
|
||||
return False
|
||||
|
||||
def _render_template(config_file):
|
||||
'''
|
||||
Render config template, substituting grains where found.
|
||||
'''
|
||||
dirname, filename = os.path.split(config_file)
|
||||
env = Environment(loader=FileSystemLoader(dirname))
|
||||
template = env.get_template(filename)
|
||||
config = template.render(__grains__)
|
||||
return config
|
||||
|
||||
def _config(name, conf):
|
||||
'''
|
||||
Return a value for 'name' from the config file options.
|
||||
'''
|
||||
try:
|
||||
value = conf[name]
|
||||
except KeyError:
|
||||
value = None
|
||||
return value
|
||||
|
||||
def _result_to_dict(data, attrs=None):
|
||||
'''
|
||||
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
|
||||
For example, search result:
|
||||
|
||||
saltKeyValue': ['ntpserver=ntp.acme.local', 'foo=myfoo']
|
||||
|
||||
is written to the pillar data dictionary as:
|
||||
|
||||
{'ntpserver': 'ntp.acme.local', 'foo': 'myfoo'}
|
||||
'''
|
||||
|
||||
if not attrs:
|
||||
attrs = []
|
||||
result = {}
|
||||
for key in data:
|
||||
if key in attrs:
|
||||
for item in data.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
|
||||
|
||||
def _do_search(conf):
|
||||
'''
|
||||
Builds connection and search arguments, performs the LDAP search and
|
||||
formats the results as a dictionary appropriate for pillar use.
|
||||
'''
|
||||
# Build LDAP connection args
|
||||
connargs = {}
|
||||
for name in ['server', 'port', 'tls', 'binddn', 'bindpw']:
|
||||
connargs[name] = _config(name, conf)
|
||||
# Build search args
|
||||
try:
|
||||
filter = conf['filter']
|
||||
except KeyError:
|
||||
raise SaltInvocationError('missing filter')
|
||||
dn = _config('dn', conf)
|
||||
scope = _config('scope', conf)
|
||||
attrs = _config('attrs', conf)
|
||||
# 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))
|
||||
except Exception:
|
||||
msg = traceback.format_exc()
|
||||
log.critical('Failed to retrieve pillar data from LDAP: {0}'.format(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 {}
|
||||
opts['conf_file'] = config_file
|
||||
except Exception as e:
|
||||
import salt.log
|
||||
msg = 'Error parsing configuration file: {0} - {1}'
|
||||
if salt.log.is_console_configured():
|
||||
log.warn(msg.format(config_file, e))
|
||||
else:
|
||||
print(msg.format(config_file, e))
|
||||
else:
|
||||
log.debug('Missing configuration file: {0}'.format(config_file))
|
||||
|
||||
data = {}
|
||||
for source in opts['search_order']:
|
||||
config = opts[source]
|
||||
result = _do_search(config)
|
||||
if result:
|
||||
data.update(result)
|
||||
return data
|
|
@ -23,12 +23,16 @@ as either absent or present
|
|||
user.absent
|
||||
'''
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def _changes(
|
||||
name,
|
||||
uid=None,
|
||||
gid=None,
|
||||
groups=None,
|
||||
optional_groups=None,
|
||||
home=True,
|
||||
password=None,
|
||||
enforce_password=True,
|
||||
|
@ -55,15 +59,17 @@ def _changes(
|
|||
# Scan over the users
|
||||
if lusr['name'] == name:
|
||||
found = True
|
||||
wanted_groups = sorted(
|
||||
list(set((groups or []) + (optional_groups or []))))
|
||||
if uid:
|
||||
if lusr['uid'] != uid:
|
||||
change['uid'] = uid
|
||||
if gid:
|
||||
if lusr['gid'] != gid:
|
||||
change['gid'] = gid
|
||||
if groups:
|
||||
if lusr['groups'] != sorted(groups):
|
||||
change['groups'] = groups
|
||||
if wanted_groups:
|
||||
if lusr['groups'] != wanted_groups:
|
||||
change['groups'] = wanted_groups
|
||||
if home:
|
||||
if lusr['home'] != home:
|
||||
if not home is True:
|
||||
|
@ -103,6 +109,7 @@ def present(
|
|||
gid=None,
|
||||
gid_from_name=False,
|
||||
groups=None,
|
||||
optional_groups=None,
|
||||
home=True,
|
||||
password=None,
|
||||
enforce_password=True,
|
||||
|
@ -129,10 +136,20 @@ def present(
|
|||
The default group id
|
||||
|
||||
gid_from_name
|
||||
If True, the default group id will be set to the id of the group with the same name as the user.
|
||||
If True, the default group id will be set to the id of the group with
|
||||
the same name as the user.
|
||||
|
||||
groups
|
||||
A list of groups to assign the user to, pass a list object
|
||||
A list of groups to assign the user to, pass a list object. If a group
|
||||
specified here does not exist on the minion, the state will fail.
|
||||
|
||||
optional_groups
|
||||
A list of groups to assign the user to, pass a list object. If a group
|
||||
specified here does not exist on the minion, the state will silently
|
||||
ignore it.
|
||||
|
||||
NOTE: If the same group is specified in both "groups" and
|
||||
"optional_groups", then it will be assumed to be required and not optional.
|
||||
|
||||
home
|
||||
The location of the home directory to manage
|
||||
|
@ -182,6 +199,32 @@ def present(
|
|||
'result': True,
|
||||
'comment': 'User {0} is present and up to date'.format(name)}
|
||||
|
||||
if groups:
|
||||
missing_groups = [x for x in groups if not __salt__['group.info'](x)]
|
||||
if missing_groups:
|
||||
ret['comment'] = 'The following group(s) are not present: ' \
|
||||
'{0}'.format(','.join(missing_groups))
|
||||
ret['result'] = False
|
||||
return ret
|
||||
|
||||
if optional_groups:
|
||||
present_optgroups = [x for x in optional_groups
|
||||
if __salt__['group.info'](x)]
|
||||
for missing_optgroup in [x for x in optional_groups
|
||||
if x not in present_optgroups]:
|
||||
log.debug('Optional group "{0}" for user "{1}" is not '
|
||||
'present'.format(missing_optgroup,name))
|
||||
else:
|
||||
present_optgroups = None
|
||||
|
||||
|
||||
# Log a warning for all groups specified in both "groups" and
|
||||
# "optional_groups" lists.
|
||||
if groups and optional_groups:
|
||||
for x in set(groups).intersection(optional_groups):
|
||||
log.warning('Group "{0}" specified in both groups and '
|
||||
'optional_groups for user {1}'.format(x,name))
|
||||
|
||||
if gid_from_name:
|
||||
gid = __salt__['file.group_to_gid'](name)
|
||||
changes = _changes(
|
||||
|
@ -189,6 +232,7 @@ def present(
|
|||
uid,
|
||||
gid,
|
||||
groups,
|
||||
present_optgroups,
|
||||
home,
|
||||
password,
|
||||
enforce_password,
|
||||
|
|
|
@ -487,6 +487,12 @@ class ExtendedTargetOptionsMixIn(TargetOptionsMixIn):
|
|||
'for the target is the pillar key followed by a glob'
|
||||
'expression:\n"role:production*"')
|
||||
)
|
||||
group.add_option(
|
||||
'-S', '--ipcidr',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=('Match based on Subnet (CIDR notation) or IPv4 address.')
|
||||
)
|
||||
|
||||
self._create_process_functions()
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue