source: dassldapsync/dassldapsync.py@ 1260

Last change on this file since 1260 was 1260, checked in by joergs, on May 3, 2022 at 7:55:52 PM

adapted a bit for Python 2, but still not runnable.

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