summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@fripost.org>2013-12-02 05:56:20 +0100
committerGuilhem Moulin <guilhem@fripost.org>2015-06-07 02:51:05 +0200
commitdd155fee24fcb05dad7ea9df241ce138ad7083b0 (patch)
treee411ba979f2792914896223f94f0b0bdc9dabf95
parent5a7bec1a590e20e263d41eaf414cfe9b5ba48a75 (diff)
Automatically configure Overlays.
A 'suffix=' parameter has been added to choose the database to configure the overlay for. The ability to delete overlays would be desirable, but sadly there is no cleane way to remove/replace overlays, short of stopping slapd and digging into the slapd.d directory: http://www.zytrax.com/books/ldap/ch6/slapd-config.html#use-overlays
-rw-r--r--lib/openldap73
1 files changed, 47 insertions, 26 deletions
diff --git a/lib/openldap b/lib/openldap
index a90a386..6f2bb68 100644
--- a/lib/openldap
+++ b/lib/openldap
@@ -17,52 +17,54 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import ldap, ldap.sasl
from ldap.filter import filter_format
from ldap.dn import dn2str,explode_dn,str2dn
from ldap.modlist import addModlist
from ldif import LDIFParser
from functools import partial
import re, pwd
# Dirty hack to check equality between the targetted LDIF and that
# currently in the directory. The value of some configuration (olc*)
# attributes is automatically indexed when added; for those we'll add
# explicit indices to what we find in the LDIF.
indexedAttributes = frozenset([
'olcAttributeTypes',
'olcObjectClasses',
'olcAccess',
'olcSyncrepl',
+ 'olcOverlay',
])
# Another hack. Configuration entries sometimes pollutes the DNs with
# indices, thus it's not possible to directly use them as base.
# Instead, we use their parent as a pase, and search for the *unique*
# match with the same ObjectClass and the matching extra attributes.
# ('%s' in the attribute value is replaced with the value of the source
# entry.)
indexedDN = {
- 'olcSchemaConfig': [('cn', '{*}%s')],
- 'olcHdbConfig': [('olcDbDirectory', '%s' )],
+ 'olcSchemaConfig': [('cn', '{*}%s')],
+ 'olcHdbConfig': [('olcDbDirectory', '%s' )],
+ 'olcOverlayConfig': [('olcOverlay', '%s' )],
}
# Allow for flexible ACLs for user using SASL's EXTERNAL mechanism.
# "username=postfix,cn=peercred,cn=external,cn=auth" is replaced by
# "gidNumber=106+uidNumber=102,cn=peercred,cn=external,cn=auth" where
# 102 is postfix's UID and 106 its primary GID.
# (Regular expressions are not allowed.)
sasl_ext_re = re.compile( r"""(?P<start>\sby\s+dn(?:\.exact)?)=
(?P<quote>['\"]?)username=(?P<user>[a-z][-a-z0-9_]*),
(?P<end>cn=peercred,cn=external,cn=auth)
(?P=quote)\s"""
, re.VERBOSE )
pwd_dict = {}
def acl_sasl_ext(m):
u = m.group('user')
if u not in pwd_dict.keys():
pwd_dict[u] = pwd.getpwnam(u)
return '%s="gidNumber=%d+uidNumber=%d,%s" ' % ( m.group('start')
, pwd_dict[u].pw_gid
@@ -103,40 +105,41 @@ def slapcat(filter=None, suffix=None, idx=0):
cmd.append( '-n%d' % idx )
return subprocess.Popen( cmd, stdout=subprocess.PIPE
, stderr=open(os.devnull, 'wb') )
# Start / stop / whatever a service.
def service(name, state):
cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'service'), name, state ]
subprocess.check_call( cmd, stdout=open(os.devnull, 'wb')
, stderr=subprocess.STDOUT )
# Check if the given dn is already present in the directory.
# Returns None if doesn't exist, and give the dn,entry otherwise
def flexibleSearch(module, l, dn, entry):
idxClasses = set(entry['objectClass']).intersection(indexedDN.keys())
if not idxClasses:
base = dn
scope = ldap.SCOPE_BASE
+ f = 'objectClass=*'
else:
# Search on the parent instead, and try to use a precise filter
dn = str2dn(dn)
h,t,_ = dn.pop(0)[0]
base = dn2str(dn)
scope = ldap.SCOPE_ONELEVEL
f = []
for c in idxClasses:
f.append ( filter_format('objectClass=%s', [c]) )
for a,v in indexedDN[c]:
if a == h:
v2 = t
elif a not in entry.keys() or len(entry[a]) > 1:
module.fail_json(msg="Multiple values found! This is a bug. Please report.")
else:
v2 = entry[a][0]
f.append ( filter_format(a+'='+v, [v2]) )
if len(f) == 1:
f = f[0]
else:
@@ -194,77 +197,79 @@ def processEntry(module, l, dn, entry):
x = list(s1.difference(s2))
if x:
diff.append(( ldap.MOD_DELETE, a, x ))
y = list(s2.difference(s1))
if y:
diff.append(( ldap.MOD_ADD, a, y ))
# add attributes that weren't in e
for a in set(entry).difference(e.keys()):
diff.append(( ldap.MOD_ADD, a, entry[a] ))
if diff:
changed = True
if module.check_mode:
module.exit_json(changed=changed, msg="mod DN %s" % dn)
l.modify_s( d, diff )
return changed
# Load the given module.
-def loadModule(module, name):
+def loadModule(module, l, name):
changed = False
- l = ldap.initialize( 'ldapi://' )
- l.sasl_interactive_bind_s('', ldap.sasl.external())
f = filter_format( '(&(objectClass=olcModuleList)(olcModuleLoad=%s))', [name] )
- r = l.search_s( 'cn=config', ldap.SCOPE_ONELEVEL, filterstr = f, attrlist = [] )
+ r = l.search_s( 'cn=config', ldap.SCOPE_ONELEVEL, filterstr = f, attrlist = [''] )
if not r:
changed = True
if module.check_mode:
module.exit_json(changed=changed, msg="add module %s" % name)
l.modify_s( 'cn=module{0},cn=config'
, [(ldap.MOD_ADD, 'olcModuleLoad', name)] )
- l.unbind_s()
return changed
+# Find the database associated with a given attribute (eg,
+# olcDbDirectory or olcSuffix).
+def getDN_DB(module, l, a, v):
+ f = filter_format( '(&(objectClass=olcDatabaseConfig)('+a+'=%s))', [v] )
+ return l.search_s( 'cn=config'
+ , ldap.SCOPE_ONELEVEL
+ , filterstr = f
+ , attrlist = [''] )
+
+
# Clear the given DB directory and delete the associated database. Fail
# if non empty, unless all existing DNS are in skipdns.
def wontRemove(module, skipdns, d, _):
if d not in skipdns:
module.fail_json(msg="won't remove '%s'" % d)
def removeDB(module, dbdir, skipdn=None):
changed = False
if not os.path.exists(dbdir):
return False
l = ldap.initialize( 'ldapi://' )
l.sasl_interactive_bind_s('', ldap.sasl.external())
-
- f = filter_format( '(&(objectClass=olcDatabaseConfig)(olcDbDirectory=%s))', [dbdir] )
- r = l.search_s( 'cn=config'
- , ldap.SCOPE_ONELEVEL
- , filterstr = f
- , attrlist = ['olcSuffix'] )
+ r = getDN_DB( module, l, 'olcDbDirectory', dbdir )
l.unbind_s()
if len(r) > 1:
module.fail_json(msg="Multiple results found! This is a bug. Please report.")
elif r:
dn,entry = r.pop()
suffix = entry['olcSuffix'][0]
skipdns = [suffix]
if skipdn is not None:
skipdns.extend([ "%s,%s" % (s,suffix) for s in skipdn ])
# here we need to use slapcat not search_s, because we may
# not have read access on the database (even though we're
# root!).
p = slapcat( suffix=suffix )
parser = LDIFCallback( module, p.stdout
, partial(wontRemove,module,skipdns) )
parser.parse()
changed = True
@@ -278,78 +283,94 @@ def removeDB(module, dbdir, skipdn=None):
ldif = explode_dn(dn)[::-1]
ldif[-1] += ".ldif"
path.extend( ldif )
os.unlink( os.path.join(*path) )
# delete all children in path, but not the path directory itself.
for file in os.listdir(dbdir):
os.unlink( os.path.join(dbdir, file) )
service( 'slapd', 'start' )
return changed
def main():
module = AnsibleModule(
argument_spec = dict(
dbdirectory = dict( default=None ),
ignoredn = dict( default=None ),
state = dict(default="present", choices=["absent", "present"]),
target = dict( default=None ),
module = dict( default=None ),
+ suffix = dict( default=None ),
),
supports_check_mode=True
)
params = module.params
state = params['state']
dbdirectory = params['dbdirectory']
ignoredn = params['ignoredn']
target = params['target']
mod = params['module']
+ suffix = params['suffix']
if ignoredn is not None:
ignoredn = ignoredn.split(':')
changed = False
try:
if state == "absent":
if dbdirectory is not None:
changed = removeDB(module,dbdirectory,skipdn=ignoredn)
# TODO: might be useful to be able remove DNs
else:
module.fail_json(msg="missing dbdirectory")
elif state == "present":
- if target is not None:
- # bind only once per LDIF file to
- l = ldap.initialize( 'ldapi://' )
- l.sasl_interactive_bind_s('', ldap.sasl.external())
-
- parser = LDIFCallback( module, open(target, 'r')
- , partial(processEntry,module,l) )
- parser.parse()
- l.unbind_s()
- changed = parser.changed
- elif mod is not None:
- changed = loadModule(module, mod)
- else:
+ if target is None and mod is None:
module.fail_json(msg="missing target or module")
+ # bind only once per LDIF file for performance
+ l = ldap.initialize( 'ldapi://' )
+ l.sasl_interactive_bind_s('', ldap.sasl.external())
+
+ if mod is None:
+ callback = partial(processEntry,module,l)
+ else:
+ changed |= loadModule (module, l, '%s.la' % mod)
+ if target is None and suffix is None:
+ l.unbind_s()
+ module.exit_json(changed=changed)
+ if target is None or suffix is None:
+ module.fail_json(msg="missing target or suffix")
+ r = getDN_DB(module, l, 'olcSuffix', suffix)
+ if not r:
+ module.fail_json(msg="No database found for suffix %s" % suffix)
+ elif len(r) > 1:
+ module.fail_json(msg="Multiple results found! This is a bug. Please report.")
+ else:
+ d = 'olcOverlay=%s,%s' % (mod, r.pop()[0])
+ callback = lambda _,e: processEntry(module,l,d,e)
+
+ parser = LDIFCallback( module, open(target, 'r'), callback )
+ parser.parse()
+ changed = parser.changed
+ l.unbind_s()
except subprocess.CalledProcessError, e:
module.fail_json(rv=e.returncode, msg=e.output.rstrip())
except ldap.LDAPError, e:
e = e.args[0]
if 'info' in e.keys():
msg = e['info']
elif 'desc' in e.keys():
msg = e['desc']
else:
msg = str(e)
module.fail_json(msg=msg)
except KeyError, e:
module.fail_json(msg=str(e))
module.exit_json(changed=changed)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>