source: dassldapsync/dassldapsync.py@ 1259

Last change on this file since 1259 was 1259, checked in by joergs, on May 3, 2022 at 7:53:07 PM

adapted to SLES 15 and Python 3. Encoding problems with Python 2

Fixing sisabling deleting

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