source: dassldapsync/dassldapsync.py

Last change on this file was 1272, checked in by joergs, on Apr 21, 2023 at 2:35:27 PM

bugfix

  • Property svn:executable set to *
File size: 23.9 KB
RevLine 
[1218]1#!/usr/bin/python
2# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
3
[1260]4from __future__ import print_function
5
[1221]6# core modules
[1219]7import argparse
[1260]8try:
9 from configparser import ConfigParser
10except ImportError:
11 from ConfigParser import ConfigParser
[1221]12import logging
13from pprint import pprint
14import signal
15import subprocess
16import sys
17import time
18
19# external modules
[1219]20import datetime
21import dateutil.parser
22import dateutil.tz
[1218]23import ldap
[1221]24from ldap.ldapobject import ReconnectLDAPObject
[1219]25import ldap.modlist
[1221]26from ldap.syncrepl import SyncreplConsumer
[1259]27from ldapurl import LDAPUrl
[1218]28import ldif
29
[1221]30
31
[1219]32def getArguments():
33 configfile = '/etc/dassldapsync.conf'
[1221]34 parser = argparse.ArgumentParser(description='Synchronize the content of two LDAP servers.')
[1219]35 parser.add_argument('-d', '--debug', action='store_true', help="enable debug output")
[1271]36 parser.add_argument('-n', '--dry-run', action='store_true', dest='dryrun', help="dry run")
[1221]37 parser.add_argument('configfile', default=configfile,
38 help="Configuration file [default: {}]".format(configfile))
39 return parser.parse_args()
[1218]40
[1219]41
[1221]42class Options(object):
[1218]43 def __init__(self):
[1271]44 self.create = False
45 self.delete = False
[1221]46 self.starttls = False
47 self.updateonly = False
48 self.filter = None
49 self.attrlist = None
50 self.exclude = None
51 self.renameattr = None
52 self.renamecommand = None
53 self.pwd_max_days = 0
54
[1218]55def readLDIFSource(path):
[1219]56 logger = logging.getLogger()
57 logger.info("reading LDAP objects from file {}".format(path))
[1221]58 with open(path, 'r') as f:
[1218]59 parser = ldif.LDIFRecordList(f)
60 parser.parse()
61 result = parser.all_records
62 return result
63
[1221]64def readLdapSource(server, binddn, bindpw, basedn, filterstr, attrlist=None, starttls=False):
[1219]65 logger = logging.getLogger()
66 logger.info("reading LDAP objects from server {}".format(server))
[1272]67 ldapurl = LDAPUrl(hostport="{}:389".format(server))
[1259]68 con = ldap.initialize(ldapurl. initializeUrl())
[1218]69 if starttls:
[1221]70 con.start_tls_s()
71 con.simple_bind_s(binddn, bindpw)
72 results = con.search_s(basedn, ldap.SCOPE_SUBTREE, filterstr, attrlist)
[1218]73 return results
74
[1220]75class LdapSync(object):
[1221]76 def __init__(self, destserver,
77 destbinddn, destbindpw,
78 srcbasedn, destbasedn, options=Options()):
[1219]79 self.logger = logging.getLogger()
[1221]80
[1219]81 self.destserver = destserver
82 self.destbasedn = destbasedn
83 self.destbinddn = destbinddn
84 self.destbindpw = destbindpw
85 self.options = options
[1221]86
[1219]87 self.srcbasedn = srcbasedn
[1218]88
[1219]89 self.con = None
[1218]90
[1221]91 self.attrmap = ldap.cidict.cidict({})
92 self.classmap = {}
[1218]93
[1259]94 #self.junk_objectclasses = [ b"sambaidmapentry" ]
95 #"sambasid",
96 self.junk_objectclasses = []
97 self.junk_attrs = ["authzto",
98 "creatorsname", "createtimestamp", "contextcsn",
99 "entrycsn", "entryuuid",
100 "memberof", "modifiersname", "modifytimestamp",
101 "pwdaccountlockedtime", "pwdchangedtime", "pwdfailuretime",
102 "structuralobjectclass"]
[1221]103
104 self.reset_result()
105
106
107 def reset_result(self):
108 self.result = {
[1262]109 'excluded': {'ok': [], 'failed': []},
[1261]110 'unmodified': {'ok': [], 'failed': []},
[1221]111 'add': {'ok': [], 'failed': []},
112 'update': {'ok': [], 'failed': []},
113 'delete': {'ok': [], 'failed': []},
114 }
115
116
117 def _dest_ldap_connect(self):
[1219]118 if self.con is None:
119 self.logger.info("connect to destination LDAP server {}".format(self.destserver))
[1259]120 ldapurl = LDAPUrl(hostport="{}:389".format(self.destserver))
121 self.con = ldap.initialize(ldapurl. initializeUrl())
[1219]122 if self.options.starttls:
123 self.con.start_tls_s()
[1221]124 self.con.simple_bind_s(self.destbinddn, self.destbindpw)
[1218]125
[1219]126 def __adapt_dn(self, dn):
127 # move LDAP object to dest base
128 if self.srcbasedn != self.destbasedn:
129 dn_old = dn
[1221]130 rpath = dn[:-len(self.srcbasedn)]
131 dn = rpath+self.destbasedn
[1219]132 self.logger.debug("moved {} to {}".format(dn_old, dn))
133 # print "dn:",dn,"src:",srcbasedn,"rpath:",rpath,"dest:",destbasedn
134 return dn
[1218]135
[1219]136 def __is_dn_included(self, dn):
137 if self.options.exclude is None:
138 return True
139 if dn.lower().endswith(self.options.exclude):
140 return False
141 return True
[1218]142
[1219]143 def __adapt_source_ldap_objects(self, searchresult):
144 """
145 Do configured modification to the source LDAP objects.
146 """
147 self.logger.debug("modifying LDAP objects retrieved from source LDAP")
[1218]148
[1221]149 update_objects = []
150
[1219]151 for r in searchresult:
152 dn = self.__adapt_dn(r[0])
[1221]153 d = ldap.cidict.cidict(r[1])
[1218]154
[1261]155 if not self.__is_dn_included(dn):
156 self.notify_excluded(dn)
157 else:
[1221]158 objectclasses = d["objectclass"]
[1218]159
[1221]160 newObjectclasses = []
[1219]161 for o in objectclasses:
162 if o.lower() in self.classmap:
[1221]163 new_oc = self.classmap[o.lower()]
164 if new_oc not in newObjectclasses:
165 newObjectclasses.append(new_oc)
[1219]166 else:
[1221]167 if o not in newObjectclasses:
[1219]168 newObjectclasses.append(o)
169
[1221]170 d["objectclass"] = newObjectclasses
[1219]171
172 for a in d.keys():
[1221]173 attr = a
[1219]174 if self.attrmap.has_key(a.lower()):
[1221]175 attr = self.attrmap[attr].lower()
176 if attr.lower() != a.lower():
177 values = d[a]
[1219]178 del d[a]
[1221]179 d[attr] = values
[1219]180
[1221]181 update_objects.append((dn, d))
[1219]182 return update_objects
183
184
[1221]185 def _get_dest_entry(self, dn, entry):
[1219]186 """
187 In the destination LDAP, the objects should be named
188 according to options.renameattr.
189 """
[1221]190 attrlist = self.options.attrlist
191
[1219]192 existingDestDn = None
193 existingDestEntry = None
194 if self.options.renameattr and entry.has_key(self.options.renameattr):
[1221]195 searchresult = self.con.search_s(
196 self.destbasedn,
197 ldap.SCOPE_SUBTREE,
198 '%s=%s' % (self.options.renameattr, entry[self.options.renameattr][0]), attrlist)
199 if searchresult is not None and len(searchresult) > 0:
[1219]200 existingDestDn, existingDestEntry = searchresult[0]
201 if existingDestDn.lower() != dn.lower():
[1271]202 if not self.options.dryrun:
203 self.con.modrdn_s(existingDestDn, dn)
[1221]204 self.notify_renamed(existingDestDn, dn,
205 existingDestEntry[self.options.renameattr][0],
206 entry[self.options.renameattr][0],
207 options)
[1219]208 if existingDestDn is None:
[1221]209 searchresult = self.con.search_s(dn, ldap.SCOPE_BASE, 'objectclass=*', attrlist)
[1219]210 existingDestDn, existingDestEntry = searchresult[0]
211 return (existingDestDn, existingDestEntry)
212
213
214 def __handle_pwdAccountLockedTime(self, dn, entry, now, max_age):
215 # hack for syncing accounts locked by password policy
216 do_unlock = False
[1221]217 if self.options.pwd_max_days > 0 and entry.has_key('pwdChangedTime'):
[1219]218 # print "pwdChangedTime set for",dn
219 pwdChange = entry['pwdChangedTime'][0]
220 d = dateutil.parser.parse(pwdChange)
[1221]221 if (now-d) > max_age:
222 entry['pwdAccountLockedTime'] = ['000001010000Z']
223 self.logger.info("locking {} {}".format(dn, pwdChange))
[1218]224 else:
[1219]225 # pwdAccountLockedTime is a operational attribute,
226 # and therefore not part of entry.
227 # Do extra search to retrieve attribute.
[1221]228 searchresult = self.con.search_s(
229 dn, ldap.SCOPE_BASE,
230 "objectclass=*", attrlist=['pwdAccountLockedTime'])
[1219]231 tmp_dn, tmp_entry = searchresult[0]
232 if tmp_entry.has_key('pwdAccountLockedTime'):
233 do_unlock = True
234 return do_unlock
[1218]235
236
[1221]237 def _syncLdapObject(self, srcDn, srcAttributes):
[1219]238 tzutc = dateutil.tz.gettz('UTC')
239 now = datetime.datetime.now(tzutc)
240 max_age = datetime.timedelta(days=self.options.pwd_max_days)
[1218]241
[1259]242 objectClasses = srcAttributes['objectClass']
243 srcAttributes['objectClass'] = [oc for oc in objectClasses if oc.lower() not in self.junk_objectclasses]
244
[1221]245 try:
246 destDn, destAttributes = self._get_dest_entry(srcDn, srcAttributes)
[1218]247
[1221]248 # hack for syncing accounts locked by password policy
249 do_unlock = self.__handle_pwdAccountLockedTime(srcDn, srcAttributes, now, max_age)
[1218]250
[1221]251 mod_attrs = ldap.modlist.modifyModlist(destAttributes, srcAttributes)
[1218]252
[1221]253 # hack for unlocking, see above
254 if do_unlock:
255 self.logger.info("unlocking {} {}".format(destDn, 'pwdAccountLockedTime'))
256 mod_attrs.append((ldap.MOD_DELETE, 'pwdAccountLockedTime', None))
[1218]257
[1221]258 if self.options.attrlist is not None:
259 mod_attrs = [a for a in mod_attrs if a[1].lower() in self.options.attrlist]
[1218]260
[1221]261 if self.junk_attrs is not None:
262 mod_attrs = [a for a in mod_attrs if a[1].lower() not in self.junk_attrs]
[1218]263
[1221]264 if mod_attrs:
265 try:
266 self.logger.debug('mod_attrs: ' + str(mod_attrs))
[1271]267 if not self.options.dryrun:
268 self.con.modify_s(srcDn, mod_attrs)
[1221]269 self.notify_modified(srcDn)
270 except:
271 self.logger.exception('modify failed')
272 self.notify_modified(srcDn, False)
[1259]273 else:
274 self.notify_unchanged(srcDn)
[1218]275
[1221]276 except ldap.NO_SUCH_OBJECT:
[1271]277 if self.options.create:
[1219]278 try:
[1259]279 entry = ldap.modlist.addModlist(srcAttributes, self.junk_attrs)
[1271]280 if not self.options.dryrun:
281 self.con.add_s(srcDn, entry)
[1221]282 self.notify_created(srcDn)
283 except (ldap.OBJECT_CLASS_VIOLATION,
284 ldap.NO_SUCH_OBJECT,
[1259]285 ldap.CONSTRAINT_VIOLATION) as e:
286 #print(e)
[1221]287 self.notify_created(srcDn, False)
[1218]288
[1221]289
290 def __syncLdapDestination(self, update_objects):
291 logger.debug("writing data to destination LDAP")
292 for obj in update_objects:
293 dn, entry = obj
294 self._syncLdapObject(dn, entry)
295
296
[1219]297 def __deleteDestLdapObjects(self, update_objects):
298 """
299 Remove all LDAP objects in destination LDAP server
300 that did not come from the source LDAP objects
301 and are not excluded.
302 """
[1218]303
[1221]304 searchresult = self.con.search_s(self.destbasedn, ldap.SCOPE_SUBTREE, self.options.filter)
305 existing = [x[0].lower() for x in searchresult]
[1218]306
[1221]307 morituri = existing
[1218]308
[1219]309 if self.destbasedn.lower() in existing:
310 morituri.remove(self.destbasedn.lower())
[1218]311
[1221]312 for obj in update_objects:
313 dn, entry = obj
[1218]314 if dn.lower() in existing:
315 morituri.remove(dn.lower())
316 for dn in morituri:
[1219]317 if self.__is_dn_included(dn):
318 try:
[1271]319 if not self.options.dryrun:
320 self.con.delete_s(dn)
[1221]321 self.notify_deleted(dn)
[1219]322 except:
[1221]323 self.notify_deleted(dn, False)
[1218]324
[1221]325
[1219]326 def sync(self, searchresult):
327 """
328 Synchronize entries from searchresult to destination LDAP server.
329 """
[1221]330 if len(searchresult) == 0:
[1219]331 self.logger.error("empty source, aborting")
332 return
333
[1221]334 self._dest_ldap_connect()
[1219]335
336 update_objects = self.__adapt_source_ldap_objects(searchresult)
[1221]337 self.__syncLdapDestination(update_objects)
[1271]338 if self.options.delete:
[1221]339 self.__deleteDestLdapObjects(update_objects)
[1219]340 self.con.unbind()
[1218]341
[1221]342 self.__log_summary(True)
[1218]343
[1219]344
[1221]345 def __log_summary(self, show_failed=True, show_ok=False):
346 result = self.result
[1219]347 for action in result.keys():
348 ok = len(result[action]['ok'])
349 failed = len(result[action]['failed'])
[1261]350 print("{} (ok: {}, failed: {})".format(action, ok, failed))
[1219]351
[1261]352 if ok > 0 and (show_ok or ok <= 3):
[1259]353 print("succeeded:")
354 print("\n".join(result[action]['ok']))
[1219]355
[1261]356 if failed > 0 and (show_failed or failed <= 3):
[1259]357 print("failed:")
358 print("\n".join(result[action]['failed']))
[1261]359 print()
[1219]360
[1221]361 def get_short_dn(self, dn):
362 return dn.lower().replace(',' + self.srcbasedn.lower(), '')
[1219]363
[1259]364 def notify_unchanged(self, dn):
[1261]365 #logger.debug(u'{} unchanged'.format(self.get_short_dn(dn)))
366 self.result['unmodified']['ok'].append(dn)
[1259]367
[1261]368 def notify_excluded(self, dn):
369 #logger.debug(u'{} unchanged'.format(self.get_short_dn(dn)))
370 self.result['excluded']['ok'].append(dn)
371
[1221]372 def notify_created(self, dn, ok=True):
373 if ok:
[1259]374 logger.debug(u'{} created'.format(self.get_short_dn(dn)))
[1221]375 self.result['add']['ok'].append(dn)
376 else:
[1259]377 self.logger.warning(u"failed to add {}".format(dn))
[1221]378 self.result['add']['failed'].append(dn)
[1219]379
[1221]380 def notify_modified(self, dn, ok=True):
381 if ok:
[1259]382 logger.debug(u'{} modified'.format(self.get_short_dn(dn)))
[1221]383 self.result['update']['ok'].append(dn)
384 else:
[1259]385 self.logger.error(u"failed to modify {}".format(dn))
[1221]386 self.result['update']['failed'].append(dn)
[1219]387
[1221]388 def notify_deleted(self, dn, ok=True):
389 if ok:
[1259]390 logger.debug(u'{} deleted'.format(self.get_short_dn(dn)))
[1221]391 self.result['delete']['ok'].append(dn)
392 else:
[1259]393 self.logger.error(u"failed to delete {}".format(dn))
[1221]394 self.result['delete']['failed'].append(dn)
[1219]395
396 def notify_renamed(self, dn, newdn, uid, newuid, options):
[1259]397 print(u"renamed {} -> {}".format(dn, newdn))
[1221]398 subprocess.check_call(
399 "%s %s %s %s %s" % (options.renamecommand, dn, newdn, uid, newuid),
400 shell=True)
[1219]401
402
[1221]403
404class SyncReplConsumer(ReconnectLDAPObject, SyncreplConsumer):
405 """
406 Syncrepl Consumer interface
407 """
408
409 def __init__(self, dest, syncrepl_entry_callback, *args, **kwargs):
410 self.logger = logging.getLogger()
411 # Initialise the LDAP Connection first
412 ldap.ldapobject.ReconnectLDAPObject.__init__(self, *args, **kwargs)
413 # We need this for later internal use
414 self.__presentUUIDs = dict()
415 self.cookie = None
416 self.dest_ldap = dest
417 self.syncrepl_entry_callback = syncrepl_entry_callback
418
419 def syncrepl_get_cookie(self):
420 return self.cookie
421
422 def syncrepl_set_cookie(self, cookie):
423 self.cookie = cookie
424
425 def syncrepl_entry(self, dn, attributes, uuid):
426 # First we determine the type of change we have here
427 # (and store away the previous data for later if needed)
428 if uuid in self.__presentUUIDs:
429 change_type = 'modify'
430 else:
431 change_type = 'add'
432 # Now we store our knowledge of the existence of this entry
433 self.__presentUUIDs[uuid] = dn
434 # Debugging
435 logger.debug('{}: {} ({})'.format(dn, change_type, ",".join(attributes.keys())))
436 # If we have a cookie then this is not our first time being run,
437 # so it must be a change
438 if self.cookie is not None:
439 self.syncrepl_entry_callback(dn, attributes)
440
441
442 def syncrepl_delete(self, uuids):
443 """ syncrepl_delete """
444 # Make sure we know about the UUID being deleted, just in case...
445 uuids = [uuid for uuid in uuids if uuid in self.__presentUUIDs]
446 # Delete all the UUID values we know of
447 for uuid in uuids:
448 logger.debug('detected deletion of entry {} ({})', uuid, self.__presentUUIDs[uuid])
449 del self.__presentUUIDs[uuid]
450
451 def syncrepl_present(self, uuids, refreshDeletes=False):
452 """ called on initial sync """
453 if uuids is not None:
454 self.logger.debug('uuids: {}'.format(','.join(uuids)))
455 # If we have not been given any UUID values,
456 # then we have recieved all the present controls...
457 if uuids is None:
458 # We only do things if refreshDeletes is false as the syncrepl
459 # extension will call syncrepl_delete instead when it detects a
460 # delete notice
461 if not refreshDeletes:
462 deletedEntries = [
463 uuid for uuid in self.__presentUUIDs
464 ]
465 self.syncrepl_delete(deletedEntries)
466 # Phase is now completed, reset the list
467 self.__presentUUIDs = {}
468 else:
469 # Note down all the UUIDs we have been sent
470 for uuid in uuids:
471 self.__presentUUIDs[uuid] = True
472
473
474 def syncrepl_refreshdone(self):
475 self.logger.info('Initial synchronization is now done, persist phase begins')
476 #self.logger.debug('UUIDs:\n' + '\n'.join(self.__presentUUIDs))
477
478
479
480class LdapSyncRepl(LdapSync):
481 def __init__(self, destsrv,
482 destadmindn, destadminpw,
483 basedn, destbasedn,
484 options=Options(), source_ldap_url_obj=None):
485 # Install our signal handlers
486 signal.signal(signal.SIGTERM, self.shutdown)
487 self.watcher_running = False
488 self.source_ldap_url_obj = source_ldap_url_obj
489 self.ldap_credentials = False
490 self.source_ldap_connection = None
491 super(LdapSyncRepl, self).__init__(destsrv,
492 destadmindn, destadminpw,
493 basedn, destbasedn, options)
494
495
496 def sync(self):
497 self._dest_ldap_connect()
498 self.watcher_running = True
499 while self.watcher_running:
500 self.logger.info('Connecting to source LDAP server')
501 # Prepare the LDAP server connection (triggers the connection as well)
502 self.source_ldap_connection = SyncReplConsumer(self.con,
503 self.perform_application_sync_callback,
504 self.source_ldap_url_obj.initializeUrl())
505
506 if self.source_ldap_url_obj.who and self.source_ldap_url_obj.cred:
507 self.ldap_credentials = True
508 # Now we login to the LDAP server
509 try:
510 self.source_ldap_connection.simple_bind_s(
511 self.source_ldap_url_obj.who, self.source_ldap_url_obj.cred)
[1259]512 except ldap.INVALID_CREDENTIALS as e:
513 print('Login to LDAP server failed: ', str(e))
[1221]514 sys.exit(1)
515 except ldap.SERVER_DOWN:
[1259]516 print('LDAP server is down, going to retry.')
[1221]517 time.sleep(5)
518 continue
519
520 # Commence the syncing
521 self.logger.info('Staring sync process')
522 ldap_search = self.source_ldap_connection.syncrepl_search(
523 self.source_ldap_url_obj.dn or '',
524 self.source_ldap_url_obj.scope or ldap.SCOPE_SUBTREE,
525 mode='refreshAndPersist',
526 attrlist=self.source_ldap_url_obj.attrs,
527 filterstr=self.source_ldap_url_obj.filterstr or '(objectClass=*)'
528 )
529
530 try:
531 while self.source_ldap_connection.syncrepl_poll(all=1, msgid=ldap_search):
[1259]532 print(".", end="")
[1221]533 except KeyboardInterrupt:
534 # User asked to exit
[1259]535 print("aborted\n")
[1221]536 self.shutdown(None, None)
[1259]537 except Exception as e:
[1221]538 # Handle any exception
539 if self.watcher_running:
540 self.logger.exception('Encountered a problem, going to retry.')
541 time.sleep(5)
542
543 def perform_application_sync_callback(self, dn, attributes):
544 logger.debug('{}: src: {}'.format(dn, str(attributes)))
545 try:
546 self._syncLdapObject(dn, attributes)
547 except ldap.NO_SUCH_OBJECT:
548 self.logger.info("SKIPPED: {} object does not exist on target".format(dn))
549 return False
550 return True
551
552 def shutdown(self, signum, stack):
553 # Declare the needed global variables
554 self.logger.info('Shutting down!')
555
556 # We are no longer running
557 self.watcher_running = False
558
[1259]559def get_ldap_url_obj(self, configsection):
560 baseurl = 'ldap://{server}:389/{basedn}'.format(server=configsection.get('server'), basedn=configsection.get('basedn'))
561 attrs = None
562 if configsection.get('attributes') is not None:
563 attrs = configsection.get('attributes').split(',')
564 return LDAPUrl(
565 baseurl,
566 dn=configsection.get('baseDn'),
567 who=configsection.get('bindDn'),
568 cred=configsection.get('basePassword'),
569 filterstr=configsection.get('filter'),
570 attrs=attrs
571 )
[1221]572
[1259]573
[1218]574if __name__ == "__main__":
[1219]575 logging.basicConfig(format='%(levelname)s %(module)s.%(funcName)s: %(message)s', level=logging.INFO)
576 logger = logging.getLogger()
577
[1221]578 args = getArguments()
[1219]579 if args.debug:
580 logger.setLevel(logging.DEBUG)
[1221]581 conffile = args.configfile
[1219]582
[1259]583 config = ConfigParser()
[1218]584 config.read(conffile)
585
586 srcfile = None
587 try:
[1221]588 srcfile = config.get("source", "file")
[1218]589 except:
590 pass
591
[1221]592 basedn = config.get("source", "baseDn")
[1259]593 filterstr = config.get("source", "filter", fallback=None)
[1218]594
[1221]595 if srcfile is None:
596 srv = config.get("source", "server")
597 admindn = config.get("source", "bindDn")
598 adminpw = config.get("source", "bindPassword")
599 starttls = config.getboolean("source", "starttls")
[1218]600
[1221]601 destsrv = config.get("destination", "server")
602 destadmindn = config.get("destination", "bindDn")
603 destadminpw = config.get("destination", "bindPassword")
604 destbasedn = config.get("destination", "baseDn")
[1218]605 try:
[1221]606 rdn = config.get("destination", "rdn")
[1219]607 logger.warning("setting rdn is currently ignored")
[1218]608 except:
[1219]609 pass
[1218]610
[1219]611 options = Options()
[1218]612 try:
[1221]613 options.exclude = config.get("destination", "excludesubtree").lower()
[1218]614 except:
[1219]615 pass
[1218]616
[1271]617 options.dryrun = args.dryrun
618 options.create = config.getboolean("destination", "create", fallback=False)
[1259]619 options.delete = config.getboolean("destination", "delete", fallback=False)
620 options.starttls = config.getboolean("destination", "starttls", fallback=False)
621 options.renameattr = config.get("destination", "detectRename", fallback=None)
622 options.renamecommand = config.get("destination", "detectRename", fallback=None)
623 options.pwd_max_days = int(config.get("source", "pwd_max_days", fallback=0))
[1221]624 options.filter = filterstr
[1219]625
[1221]626 # Set source.attrlist as global option.
627 # If source would use less attributes than dest,
628 # all attributes not retrieved from source would be deleted from dest
[1218]629 try:
[1221]630 options.attrlist = config.get("source", "attributes").split(",")
[1218]631 except:
[1221]632 options.attrlist = None
[1218]633
[1259]634 if config.get('source', 'mode', fallback=None) == 'syncrepl':
[1221]635 ldapsync = LdapSyncRepl(
636 destsrv, destadmindn, destadminpw, basedn, destbasedn,
637 options,
[1259]638 source_ldap_url_obj=get_ldap_url_obj(config['source']))
[1221]639 ldapsync.sync()
[1218]640 else:
[1221]641 if srcfile:
642 objects = readLDIFSource(srcfile)
643 else:
644 objects = readLdapSource(srv, admindn, adminpw,
645 basedn, filterstr, options.attrlist, starttls)
[1218]646
[1221]647 ldapsync = LdapSync(destsrv, destadmindn, destadminpw, basedn, destbasedn, options)
648 ldapsync.sync(objects)
Note: See TracBrowser for help on using the repository browser.