1 | #!/usr/bin/python
|
---|
2 | # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
|
---|
3 |
|
---|
4 | import ldap
|
---|
5 | import ldif
|
---|
6 | import ldap.modlist
|
---|
7 | import ConfigParser
|
---|
8 | import os
|
---|
9 | import sys
|
---|
10 |
|
---|
11 | import dateutil.parser
|
---|
12 | import dateutil.tz
|
---|
13 | import datetime
|
---|
14 |
|
---|
15 | class Options:
|
---|
16 | def __init__(self):
|
---|
17 | self.delete=True
|
---|
18 | self.starttls=False
|
---|
19 | self.updateonly=False
|
---|
20 | self.attrfilter=None
|
---|
21 | self.exclude=None
|
---|
22 | self.renameattr=None
|
---|
23 | self.renamecommand=None
|
---|
24 | self.verbose=False
|
---|
25 |
|
---|
26 | def notify_created(dn):
|
---|
27 | print "notify_created",dn
|
---|
28 |
|
---|
29 | def notify_modified(dn):
|
---|
30 | print "notify_modified",dn
|
---|
31 |
|
---|
32 | def notify_deleted(dn):
|
---|
33 | print "notify_deleted",dn
|
---|
34 |
|
---|
35 | def notify_renamed(dn,newdn,uid,newuid,options):
|
---|
36 | print "notify_renamed",dn,newdn
|
---|
37 | subprocess.check_call("%s %s %s %s %s" % (options.renamecommand,dn,newdn,uid,newuid),shell=True)
|
---|
38 |
|
---|
39 | def readLDIFSource(path):
|
---|
40 | with open(path,'r') as f:
|
---|
41 | parser = ldif.LDIFRecordList(f)
|
---|
42 | parser.parse()
|
---|
43 | result = parser.all_records
|
---|
44 | return result
|
---|
45 |
|
---|
46 | def readLdapSource(server,binddn,bindpw,basedn,filter,starttls=False):
|
---|
47 | con = ldap.open(server,port=389)
|
---|
48 | if starttls:
|
---|
49 | con.start_tls_s()
|
---|
50 | con.simple_bind_s(binddn,bindpw)
|
---|
51 | results=con.search_s(basedn,ldap.SCOPE_SUBTREE,filter,None)
|
---|
52 | return results
|
---|
53 |
|
---|
54 | def syncLdapDestination(searchresult,destserver,destbinddn,destbindpw,srcbasedn,destbasedn,destrdn,options=Options()):
|
---|
55 |
|
---|
56 | attrmap=ldap.cidict.cidict({
|
---|
57 | })
|
---|
58 | classmap={
|
---|
59 | }
|
---|
60 |
|
---|
61 | junk_attrs = [ "memberof", "modifiersname", "modifytimestamp", "entryuuid", "entrycsn", "contextcsn", "creatorsname", "createtimestamp", "structuralobjectclass", "pwdchangedtime", "pwdfailuretime" ]
|
---|
62 | update_objects=[]
|
---|
63 |
|
---|
64 | if len(searchresult)==0:
|
---|
65 | print "empty source, aborting"
|
---|
66 | return
|
---|
67 |
|
---|
68 | for r in searchresult:
|
---|
69 | dn=r[0]
|
---|
70 |
|
---|
71 | d=ldap.cidict.cidict(r[1])
|
---|
72 | objectclasses=d["objectclass"]
|
---|
73 |
|
---|
74 | newObjectclasses=[]
|
---|
75 | for o in objectclasses:
|
---|
76 | if o.lower() in classmap:
|
---|
77 | new_oc = classmap[o.lower()]
|
---|
78 | if not new_oc in newObjectclasses:
|
---|
79 | newObjectclasses.append(new_oc)
|
---|
80 | else:
|
---|
81 | #pass
|
---|
82 | if not o in newObjectclasses:
|
---|
83 | newObjectclasses.append(o)
|
---|
84 |
|
---|
85 | d["objectclass"]=newObjectclasses
|
---|
86 |
|
---|
87 | rpath = dn[:-len(srcbasedn)]
|
---|
88 | # print "dn:",dn,"src:",srcbasedn,"rpath:",rpath,"dest:",destbasedn
|
---|
89 |
|
---|
90 | for a in d.keys():
|
---|
91 | attr=a
|
---|
92 | if attrmap.has_key(a.lower()):
|
---|
93 | attr=attrmap[attr].lower()
|
---|
94 | if attr.lower()!=a.lower():
|
---|
95 | # print "# ",a," -> ",attr
|
---|
96 | values=d[a]
|
---|
97 | del d[a]
|
---|
98 | d[attr]=values
|
---|
99 | else:
|
---|
100 | # del d[a]
|
---|
101 | continue
|
---|
102 |
|
---|
103 | dn=rpath+destbasedn
|
---|
104 |
|
---|
105 | update_objects.append((dn,d))
|
---|
106 |
|
---|
107 | con = ldap.open(destserver,port=389)
|
---|
108 | if options.starttls:
|
---|
109 | con.start_tls_s()
|
---|
110 | con.simple_bind_s(destbinddn,destbindpw)
|
---|
111 |
|
---|
112 | exist=0
|
---|
113 | failed=0
|
---|
114 | good=0
|
---|
115 | deleted=0
|
---|
116 | existing=[]
|
---|
117 | tzutc = dateutil.tz.gettz('UTC')
|
---|
118 | now = datetime.datetime.now(tzutc)
|
---|
119 | max_age = datetime.timedelta(days=pwd_max_days)
|
---|
120 |
|
---|
121 | for o in update_objects:
|
---|
122 | dn,entry=o
|
---|
123 | try:
|
---|
124 | result = None
|
---|
125 | if options.renameattr and entry.has_key(options.renameattr):
|
---|
126 | result = con.search_s(destbasedn,ldap.SCOPE_SUB,"%s=%s" % (options.renameattr,entry[options.renameattr][0]))
|
---|
127 | if result != None and len(result)>0:
|
---|
128 | existingDn, existingEntry = result[0]
|
---|
129 | if existingDn.lower() != dn.lower():
|
---|
130 | con.modrdn_s(existingDn,dn)
|
---|
131 | notify_renamed(existingDn,dn,existingEntry[options.renameattr][0],entry[options.renameattr][0],options)
|
---|
132 | continue
|
---|
133 |
|
---|
134 | result=con.search_s(dn,ldap.SCOPE_BASE,"objectclass=*")
|
---|
135 | destDn,destEntry=result[0]
|
---|
136 |
|
---|
137 | if options.exclude!=None and destDn.lower().endswith(options.exclude):
|
---|
138 | continue
|
---|
139 |
|
---|
140 | # hack for syncing accounts locked by password policy
|
---|
141 | do_unlock = False
|
---|
142 | if pwd_max_days>0 and entry.has_key('pwdChangedTime'):
|
---|
143 | # print "pwdChangedTime set for",dn
|
---|
144 | pwdChange = entry['pwdChangedTime'][0]
|
---|
145 | d = dateutil.parser.parse(pwdChange)
|
---|
146 | if (now-d)>max_age:
|
---|
147 | if dn.startswith('cn=haydar aldetest'):
|
---|
148 | entry['pwdAccountLockedTime']=[ '000001010000Z' ]
|
---|
149 | print "locking",dn,pwdChange
|
---|
150 | else:
|
---|
151 | result = con.search_s(dn,ldap.SCOPE_BASE,"objectclass=*", \
|
---|
152 | attrlist = [ 'pwdAccountLockedTime' ])
|
---|
153 | tmp_dn, tmp_entry = result[0]
|
---|
154 | if tmp_entry.has_key('pwdAccountLockedTime'):
|
---|
155 | print "unlocking",dn,pwdChange
|
---|
156 | do_unlock = True
|
---|
157 |
|
---|
158 | mod_attrs=ldap.modlist.modifyModlist(destEntry,entry)
|
---|
159 |
|
---|
160 | # hack for unlocking, see above
|
---|
161 | if do_unlock:
|
---|
162 | mod_attrs.append( (ldap.MOD_DELETE,'pwdAccountLockedTime',None) )
|
---|
163 |
|
---|
164 | if options.attrfilter!=None:
|
---|
165 | mod_attrs=[ a for a in mod_attrs if a[1] in options.attrfilter]
|
---|
166 |
|
---|
167 | if junk_attrs!=None:
|
---|
168 | mod_attrs=[ a for a in mod_attrs if a[1].lower() not in junk_attrs]
|
---|
169 |
|
---|
170 | if mod_attrs!=[]:
|
---|
171 | exist=exist+1
|
---|
172 | #if options.verbose:
|
---|
173 | # print dn, "already exists"
|
---|
174 | try:
|
---|
175 | # print dn,destEntry['objectClass'],entry['objectClass']
|
---|
176 | con.modify_s(dn,mod_attrs)
|
---|
177 | except:
|
---|
178 | print "error",dn,mod_attrs
|
---|
179 | notify_modified(dn)
|
---|
180 | else:
|
---|
181 | pass
|
---|
182 | # print "no changes, not modified"
|
---|
183 |
|
---|
184 | except ldap.NO_SUCH_OBJECT:
|
---|
185 | if options.updateonly==True:
|
---|
186 | continue
|
---|
187 |
|
---|
188 | try:
|
---|
189 | con.add_s(dn,ldap.modlist.addModlist(entry,junk_attrs))
|
---|
190 | notify_created(dn)
|
---|
191 | if options.verbose:
|
---|
192 | print dn,"created"
|
---|
193 | good=good+1
|
---|
194 | except (ldap.OBJECT_CLASS_VIOLATION,ldap.NO_SUCH_OBJECT):
|
---|
195 | print dn, "failed"
|
---|
196 | failed=failed+1
|
---|
197 |
|
---|
198 | if options.delete==True and options.updateonly==False:
|
---|
199 | result=con.search_s(destbasedn,ldap.SCOPE_SUBTREE,filter)
|
---|
200 | existing=[ x[0].lower() for x in result ]
|
---|
201 |
|
---|
202 | morituri=existing
|
---|
203 |
|
---|
204 | if destbasedn.lower() in existing:
|
---|
205 | morituri.remove(destbasedn.lower())
|
---|
206 |
|
---|
207 | for o in update_objects:
|
---|
208 | dn,entry=o
|
---|
209 | if dn.lower() in existing:
|
---|
210 | morituri.remove(dn.lower())
|
---|
211 | for dn in morituri:
|
---|
212 | if options.exclude != None and dn.lower().endswith(options.exclude):
|
---|
213 | # print "ignoring",dn
|
---|
214 | continue
|
---|
215 |
|
---|
216 | try:
|
---|
217 | con.delete_s(dn)
|
---|
218 | except:
|
---|
219 | print "failed to delete",dn
|
---|
220 |
|
---|
221 | notify_deleted(dn)
|
---|
222 | if options.verbose:
|
---|
223 | print dn,"deleted"
|
---|
224 | deleted=deleted+1
|
---|
225 |
|
---|
226 | con.unbind()
|
---|
227 | print good,"entries created,",exist,"updated,",deleted,"deleted,",failed,"failed."
|
---|
228 |
|
---|
229 |
|
---|
230 | if __name__ == "__main__":
|
---|
231 | conffile="ldapsync.conf"
|
---|
232 | filter = None
|
---|
233 | exclude = None
|
---|
234 | if len(sys.argv)>1:
|
---|
235 | conffile=sys.argv[1]
|
---|
236 |
|
---|
237 | config=ConfigParser.ConfigParser()
|
---|
238 | config.read(conffile)
|
---|
239 |
|
---|
240 | srcfile = None
|
---|
241 | try:
|
---|
242 | srcfile = config.get("source","file")
|
---|
243 | except:
|
---|
244 | pass
|
---|
245 |
|
---|
246 | basedn = config.get("source","baseDn")
|
---|
247 |
|
---|
248 | if srcfile==None:
|
---|
249 | srv = config.get("source","server")
|
---|
250 | admindn = config.get("source","bindDn")
|
---|
251 | adminpw = config.get("source","bindPassword")
|
---|
252 | filter = config.get("source","filter")
|
---|
253 | starttls = config.getboolean("source","starttls")
|
---|
254 |
|
---|
255 | if filter==None:
|
---|
256 | filter = '(objectClass=*)'
|
---|
257 |
|
---|
258 | options = Options()
|
---|
259 |
|
---|
260 | try:
|
---|
261 | options.exclude = config.get("destination","excludesubtree").lower()
|
---|
262 | except:
|
---|
263 | pass
|
---|
264 |
|
---|
265 | destsrv = config.get("destination","server")
|
---|
266 | destadmindn = config.get("destination","bindDn")
|
---|
267 | destadminpw = config.get("destination","bindPassword")
|
---|
268 | destbasedn = config.get("destination","baseDn")
|
---|
269 | destdelete = config.getboolean("destination","delete")
|
---|
270 | rdn = config.get("destination","rdn")
|
---|
271 |
|
---|
272 | try:
|
---|
273 | options.updateonly = not config.getboolean("destination","create")
|
---|
274 | except:
|
---|
275 | options.updateonly = False
|
---|
276 | options.starttls = config.getboolean("destination","starttls")
|
---|
277 | try:
|
---|
278 | options.attrfilter = config.get("destination","attributes").split(",")
|
---|
279 | except:
|
---|
280 | options.attrfilter = None
|
---|
281 |
|
---|
282 | try:
|
---|
283 | options.renameattr = config.get("destination","detectRename")
|
---|
284 | except:
|
---|
285 | options.renameattr = None
|
---|
286 |
|
---|
287 | try:
|
---|
288 | options.renamecommand = config.get("destination","detectRename")
|
---|
289 | except:
|
---|
290 | options.renamecommand = None
|
---|
291 |
|
---|
292 | if srcfile:
|
---|
293 | result = readLDIFSource(srcfile)
|
---|
294 | else:
|
---|
295 | result = readLdapSource(srv,admindn,adminpw,basedn,filter,starttls)
|
---|
296 |
|
---|
297 | try:
|
---|
298 | pwd_max_days = int(config.get("source","pwd_max_days"))
|
---|
299 | except:
|
---|
300 | pwd_max_days = 0
|
---|
301 |
|
---|
302 | syncLdapDestination(result,destsrv,destadmindn,destadminpw,basedn,destbasedn,rdn,options)
|
---|