Merge branch 'develop' of github.com:saltstack/salt into develop

This commit is contained in:
Pedro Algarvio 2012-10-05 00:58:58 +01:00
commit b2a0b6e5fb
22 changed files with 637 additions and 150 deletions

View file

@ -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
View file

@ -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
View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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',

View file

@ -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 ''

View file

@ -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])

View file

@ -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
View 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)

View file

@ -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

View file

@ -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

View file

@ -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():

View file

@ -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
View 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

View file

@ -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,

View file

@ -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()