Index: dassldapsync/dassldapsync.py
===================================================================
--- dassldapsync/dassldapsync.py	(revision 1220)
+++ dassldapsync/dassldapsync.py	(revision 1221)
@@ -2,39 +2,50 @@
 # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
 
+# core modules
 import argparse
 import ConfigParser
+import logging
+from   pprint import pprint
+import signal
+import subprocess
+import sys
+import time
+
+# external modules
 import datetime
 import dateutil.parser
 import dateutil.tz
 import ldap
+from   ldap.ldapobject import ReconnectLDAPObject
 import ldap.modlist
+from   ldap.syncrepl import SyncreplConsumer
+import ldapurl
 import ldif
-import logging
-import os
-import sys
+
+
 
 def getArguments():
     configfile = '/etc/dassldapsync.conf'
-    parser = argparse.ArgumentParser(description='Synchronize the content of two LDAP servers.' )
+    parser = argparse.ArgumentParser(description='Synchronize the content of two LDAP servers.')
     parser.add_argument('-d', '--debug', action='store_true', help="enable debug output")
-    parser.add_argument('configfile', default=configfile, help="Configuration file [default: {}]".format(configfile))
-    args = parser.parse_args()
-    return args
-
-
-class Options:
+    parser.add_argument('configfile', default=configfile,
+                        help="Configuration file [default: {}]".format(configfile))
+    return parser.parse_args()
+
+
+class Options(object):
     def __init__(self):
-        self.delete=True
-        self.starttls=False
-        self.updateonly=False
-        self.filter=None
-        self.attrfilter=None
-        self.exclude=None
-        self.renameattr=None
-        self.renamecommand=None
-        self.pwd_max_days=0
-        
+        self.delete = True
+        self.starttls = False
+        self.updateonly = False
+        self.filter = None
+        self.attrlist = None
+        self.exclude = None
+        self.renameattr = None
+        self.renamecommand = None
+        self.pwd_max_days = 0
+
 class ConfigParserDefaults(ConfigParser.ConfigParser, object):
-    def get(self, section, option, default = None):
+    def get(self, section, option, default=None):
         try:
             result = super(self.__class__, self).get(section, option)
@@ -46,5 +57,9 @@
         return result
 
-    def get0(self, section, option, default = None):
+    def get_section(self, section):
+        if section in self._sections:
+            return self._sections[section]
+
+    def get0(self, section, option, default=None):
         try:
             result = super(self.__class__, self).get(section, option)
@@ -53,5 +68,5 @@
         return result
 
-    def getboolean(self, section, option, default = None):
+    def getboolean(self, section, option, default=None):
         try:
             result = super(self.__class__, self).getboolean(section, option)
@@ -63,8 +78,23 @@
         return result
 
+    def get_ldap_url_obj(self, section):
+        baseurl = 'ldap://{server}:389/{basedn}'.format(**(self.get_section(section)))
+        attrs = None
+        if self.get0(section, 'attributes') is not None:
+            attrs = self.get(section, 'attributes').split(',')
+        return ldapurl.LDAPUrl(
+            baseurl,
+            dn=self.get(section, 'baseDn', ''),
+            who=self.get0(section, 'bindDn'),
+            cred=self.get0(section, 'basePassword'),
+            filterstr=self.get0(section, 'filter'),
+            attrs=attrs
+        )
+
+
 def readLDIFSource(path):
     logger = logging.getLogger()
     logger.info("reading LDAP objects from file {}".format(path))
-    with open(path,'r') as f:
+    with open(path, 'r') as f:
         parser = ldif.LDIFRecordList(f)
         parser.parse()
@@ -72,18 +102,20 @@
     return result
 
-def readLdapSource(server,binddn,bindpw,basedn,filter,attrs=None,starttls=False):
+def readLdapSource(server, binddn, bindpw, basedn, filterstr, attrlist=None, starttls=False):
     logger = logging.getLogger()
     logger.info("reading LDAP objects from server {}".format(server))
-    con = ldap.open(server,port=389)
+    con = ldap.open(server, port=389)
     if starttls:
-    	con.start_tls_s()
-    con.simple_bind_s(binddn,bindpw)
-    results=con.search_s(basedn,ldap.SCOPE_SUBTREE,filter,attrs)
+        con.start_tls_s()
+    con.simple_bind_s(binddn, bindpw)
+    results = con.search_s(basedn, ldap.SCOPE_SUBTREE, filterstr, attrlist)
     return results
 
 class LdapSync(object):
-    def __init__(self, destserver,destbinddn,destbindpw,srcbasedn,destbasedn,options=Options()):
+    def __init__(self, destserver,
+                 destbinddn, destbindpw,
+                 srcbasedn, destbasedn, options=Options()):
         self.logger = logging.getLogger()
-        
+
         self.destserver = destserver
         self.destbasedn = destbasedn
@@ -91,21 +123,34 @@
         self.destbindpw = destbindpw
         self.options = options
-        
+
         self.srcbasedn = srcbasedn
 
         self.con = None
-        
-        self.attrmap=ldap.cidict.cidict({})
-        self.classmap={}
-
-        self.junk_attrs = [ "memberof", "modifiersname", "modifytimestamp", "entryuuid", "entrycsn", "contextcsn", "creatorsname", "createtimestamp", "structuralobjectclass", "pwdchangedtime", "pwdfailuretime" ]
-
-    def __ldap_connect(self):
+
+        self.attrmap = ldap.cidict.cidict({})
+        self.classmap = {}
+
+        self.junk_attrs = ["memberof", "modifiersname", "modifytimestamp", "entryuuid",
+                           "entrycsn", "contextcsn", "creatorsname", "createtimestamp",
+                           "structuralobjectclass", "pwdchangedtime", "pwdfailuretime"]
+
+        self.reset_result()
+
+
+    def reset_result(self):
+        self.result = {
+            'add': {'ok': [], 'failed': []},
+            'update': {'ok': [], 'failed': []},
+            'delete': {'ok': [], 'failed': []},
+        }
+
+
+    def _dest_ldap_connect(self):
         if self.con is None:
             self.logger.info("connect to destination LDAP server {}".format(self.destserver))
-            self.con = ldap.open(self.destserver,port=389)
+            self.con = ldap.open(self.destserver, port=389)
             if self.options.starttls:
                 self.con.start_tls_s()
-            self.con.simple_bind_s(self.destbinddn,self.destbindpw)
+            self.con.simple_bind_s(self.destbinddn, self.destbindpw)
 
     def __adapt_dn(self, dn):
@@ -113,6 +158,6 @@
         if self.srcbasedn != self.destbasedn:
             dn_old = dn
-            rpath = dn[:-len(srcbasedn)]
-            dn=rpath+self.destbasedn
+            rpath = dn[:-len(self.srcbasedn)]
+            dn = rpath+self.destbasedn
             self.logger.debug("moved {} to {}".format(dn_old, dn))
             # print "dn:",dn,"src:",srcbasedn,"rpath:",rpath,"dest:",destbasedn
@@ -132,56 +177,62 @@
         self.logger.debug("modifying LDAP objects retrieved from source LDAP")
 
-        update_objects=[]
-        
+        update_objects = []
+
         for r in searchresult:
             dn = self.__adapt_dn(r[0])
-            d=ldap.cidict.cidict(r[1])
+            d = ldap.cidict.cidict(r[1])
 
             if self.__is_dn_included(dn):
-                objectclasses=d["objectclass"]
-
-                newObjectclasses=[]
+                objectclasses = d["objectclass"]
+
+                newObjectclasses = []
                 for o in objectclasses:
                     if o.lower() in self.classmap:
-                            new_oc = self.classmap[o.lower()]
-                            if not new_oc in newObjectclasses:
-                                newObjectclasses.append(new_oc)
+                        new_oc = self.classmap[o.lower()]
+                        if new_oc not in newObjectclasses:
+                            newObjectclasses.append(new_oc)
                     else:
-                        #pass
-                        if not o in newObjectclasses:
+                        if o not in newObjectclasses:
                             newObjectclasses.append(o)
 
-                d["objectclass"]=newObjectclasses
+                d["objectclass"] = newObjectclasses
 
                 for a in d.keys():
-                    attr=a
+                    attr = a
                     if self.attrmap.has_key(a.lower()):
-                        attr=self.attrmap[attr].lower()
-                        if attr.lower()!=a.lower():
-                            # print "# ",a," -> ",attr
-                            values=d[a]
+                        attr = self.attrmap[attr].lower()
+                        if attr.lower() != a.lower():
+                            values = d[a]
                             del d[a]
-                            d[attr]=values
-
-                update_objects.append((dn,d))
+                            d[attr] = values
+
+                update_objects.append((dn, d))
         return update_objects
 
 
-    def __get_dest_entry(self, dn, entry):
+    def _get_dest_entry(self, dn, entry):
         """
         In the destination LDAP, the objects should be named
         according to options.renameattr.
         """
+        attrlist = self.options.attrlist
+
         existingDestDn = None
         existingDestEntry = None
         if self.options.renameattr and entry.has_key(self.options.renameattr):
-            searchresult = self.con.search_s(self.destbasedn,ldap.SCOPE_SUB,"%s=%s" % (self.options.renameattr,entry[self.options.renameattr][0]))
-            if searchresult != None and len(searchresult)>0:
+            searchresult = self.con.search_s(
+                self.destbasedn,
+                ldap.SCOPE_SUBTREE,
+                '%s=%s' % (self.options.renameattr, entry[self.options.renameattr][0]), attrlist)
+            if searchresult is not None and len(searchresult) > 0:
                 existingDestDn, existingDestEntry = searchresult[0]
                 if existingDestDn.lower() != dn.lower():
-                    self.con.modrdn_s(existingDestDn,dn)
-                    notify_renamed(existingDestDn, dn, existingDestEntry[self.options.renameattr][0],entry[self.options.renameattr][0],options)
+                    self.con.modrdn_s(existingDestDn, dn)
+                    self.notify_renamed(existingDestDn, dn,
+                                        existingDestEntry[self.options.renameattr][0],
+                                        entry[self.options.renameattr][0],
+                                        options)
         if existingDestDn is None:
-            searchresult=self.con.search_s(dn,ldap.SCOPE_BASE,"objectclass=*")
+            searchresult = self.con.search_s(dn, ldap.SCOPE_BASE, 'objectclass=*', attrlist)
             existingDestDn, existingDestEntry = searchresult[0]
         return (existingDestDn, existingDestEntry)
@@ -191,17 +242,18 @@
         # hack for syncing accounts locked by password policy
         do_unlock = False
-        if self.options.pwd_max_days>0 and entry.has_key('pwdChangedTime'):
+        if self.options.pwd_max_days > 0 and entry.has_key('pwdChangedTime'):
             # print "pwdChangedTime set for",dn
             pwdChange = entry['pwdChangedTime'][0]
             d = dateutil.parser.parse(pwdChange)
-            if (now-d)>max_age:
-                entry['pwdAccountLockedTime']=['000001010000Z']
-                self.logger.info( "locking {} {}".format(dn, pwdChange))
+            if (now-d) > max_age:
+                entry['pwdAccountLockedTime'] = ['000001010000Z']
+                self.logger.info("locking {} {}".format(dn, pwdChange))
             else:
                 # pwdAccountLockedTime is a operational attribute,
                 # and therefore not part of entry.
                 # Do extra search to retrieve attribute.
-                searchresult = self.con.search_s(dn,ldap.SCOPE_BASE,"objectclass=*", \
-                attrlist = ['pwdAccountLockedTime'])
+                searchresult = self.con.search_s(
+                    dn, ldap.SCOPE_BASE,
+                    "objectclass=*", attrlist=['pwdAccountLockedTime'])
                 tmp_dn, tmp_entry = searchresult[0]
                 if tmp_entry.has_key('pwdAccountLockedTime'):
@@ -210,62 +262,56 @@
 
 
-
-    def __syncLdapDestination(self, update_objects):
-        
-        result = {
-            'add': { 'ok': [], 'failed': [] },
-            'update': { 'ok': [], 'failed': [] },
-        }
-
+    def _syncLdapObject(self, srcDn, srcAttributes):
         tzutc = dateutil.tz.gettz('UTC')
         now = datetime.datetime.now(tzutc)
         max_age = datetime.timedelta(days=self.options.pwd_max_days)
 
+        try:
+            destDn, destAttributes = self._get_dest_entry(srcDn, srcAttributes)
+
+            # hack for syncing accounts locked by password policy
+            do_unlock = self.__handle_pwdAccountLockedTime(srcDn, srcAttributes, now, max_age)
+
+            mod_attrs = ldap.modlist.modifyModlist(destAttributes, srcAttributes)
+
+            # hack for unlocking, see above
+            if do_unlock:
+                self.logger.info("unlocking {} {}".format(destDn, 'pwdAccountLockedTime'))
+                mod_attrs.append((ldap.MOD_DELETE, 'pwdAccountLockedTime', None))
+
+            if self.options.attrlist is not None:
+                mod_attrs = [a for a in mod_attrs if a[1].lower() in self.options.attrlist]
+
+            if self.junk_attrs is not None:
+                mod_attrs = [a for a in mod_attrs if a[1].lower() not in self.junk_attrs]
+
+            if mod_attrs:
+                try:
+                    self.logger.debug('mod_attrs: ' + str(mod_attrs))
+                    self.con.modify_s(srcDn, mod_attrs)
+                    self.notify_modified(srcDn)
+                except:
+                    self.logger.exception('modify failed')
+                    self.notify_modified(srcDn, False)
+
+        except ldap.NO_SUCH_OBJECT:
+            if not self.options.updateonly:
+                try:
+                    self.con.add_s(srcDn, ldap.modlist.addModlist(srcAttributes, self.junk_attrs))
+                    self.notify_created(srcDn)
+                except (ldap.OBJECT_CLASS_VIOLATION,
+                        ldap.NO_SUCH_OBJECT,
+                        ldap.CONSTRAINT_VIOLATION):
+                    self.notify_created(srcDn, False)
+
+
+    def __syncLdapDestination(self, update_objects):
+
         logger.debug("writing data to destination LDAP")
-        for o in update_objects:
-            dn,entry=o
-            #logger.debug(dn)
-            try:
-                destDn,destEntry=self.__get_dest_entry(dn, entry)
-
-                # hack for syncing accounts locked by password policy
-                do_unlock = self.__handle_pwdAccountLockedTime(dn, entry, now, max_age)
-    
-                mod_attrs=ldap.modlist.modifyModlist(destEntry,entry)
-
-                # hack for unlocking, see above
-                if do_unlock:
-                    self.logger.info( "unlocking {} {}".format(dn,pwdChange))
-                    mod_attrs.append( (ldap.MOD_DELETE,'pwdAccountLockedTime',None) )
-
-                if self.options.attrfilter is not None:
-                    mod_attrs=[ a for a in mod_attrs if a[1].lower() in options.attrfilter]
-
-                if self.junk_attrs is not None:
-                    mod_attrs=[ a for a in mod_attrs if a[1].lower() not in self.junk_attrs]
-
-                if mod_attrs:
-                    try:
-                            self.con.modify_s(dn,mod_attrs)
-                            result['update']['ok'].append(dn)
-                            self.notify_modified(dn)
-                    except:
-                            self.logger.error("failed to modify {} ({})".format(dn,mod_attrs))
-                            result['update']['failed'].append(dn)
-
-            except ldap.NO_SUCH_OBJECT:
-                if options.updateonly==True:
-                    continue
-
-                try:
-                    self.con.add_s(dn,ldap.modlist.addModlist(entry,self.junk_attrs))
-                    self.notify_created(dn)
-                    result['add']['ok'].append(dn)
-                except (ldap.OBJECT_CLASS_VIOLATION, ldap.NO_SUCH_OBJECT, ldap.CONSTRAINT_VIOLATION):
-                    self.logger.warning("failed to add {}".format(dn))
-                    result['add']['failed'].append(dn)
-        return result
-
-                    
+        for obj in update_objects:
+            dn, entry = obj
+            self._syncLdapObject(dn, entry)
+
+
     def __deleteDestLdapObjects(self, update_objects):
         """
@@ -275,18 +321,14 @@
         """
 
-        result = {
-            'delete': { 'ok': [], 'failed': [] },
-        }
-
-        searchresult=self.con.search_s(self.destbasedn,ldap.SCOPE_SUBTREE,self.options.filter)
-        existing=[ x[0].lower() for x in searchresult ]
-
-        morituri=existing
+        searchresult = self.con.search_s(self.destbasedn, ldap.SCOPE_SUBTREE, self.options.filter)
+        existing = [x[0].lower() for x in searchresult]
+
+        morituri = existing
 
         if self.destbasedn.lower() in existing:
             morituri.remove(self.destbasedn.lower())
 
-        for o in update_objects:
-            dn,entry=o
+        for obj in update_objects:
+            dn, entry = obj
             if dn.lower() in existing:
                 morituri.remove(dn.lower())
@@ -295,11 +337,8 @@
                 try:
                     self.con.delete_s(dn)
-                    self.notify_deleted(dn) 
-                    result['delete']['ok'].append(dn)
+                    self.notify_deleted(dn)
                 except:
-                    self.logger.error("failed to delete {}".format(dn))
-                    result['delete']['failed'].append(dn)
-        return result
-                    
+                    self.notify_deleted(dn, False)
+
 
     def sync(self, searchresult):
@@ -307,27 +346,21 @@
         Synchronize entries from searchresult to destination LDAP server.
         """
-        result = {
-            'add': { 'ok': [], 'failed': [] },
-            'update': { 'ok': [], 'failed': [] },
-            'delete': { 'ok': [], 'failed': [] },
-        }
-
-        if len(searchresult)==0:
+        if len(searchresult) == 0:
             self.logger.error("empty source, aborting")
             return
 
-        self.__ldap_connect()
+        self._dest_ldap_connect()
 
         update_objects = self.__adapt_source_ldap_objects(searchresult)
-        result = self.__syncLdapDestination(update_objects)
-        if self.options.delete==True and self.options.updateonly==False:
-            result.update(self.__deleteDestLdapObjects(update_objects))
+        self.__syncLdapDestination(update_objects)
+        if self.options.delete and not self.options.updateonly:
+            self.__deleteDestLdapObjects(update_objects)
         self.con.unbind()
- 
-        self.__log_summary(result, True)
-
-
-
-    def __log_summary(self, result, show_failed = True, show_ok = False):
+
+        self.__log_summary(True)
+
+
+    def __log_summary(self, show_failed=True, show_ok=False):
+        result = self.result
         for action in result.keys():
             ok = len(result[action]['ok'])
@@ -343,17 +376,193 @@
                 print "\n".join(result[action]['failed'])
 
-
-    def notify_created(self, dn):
-        print "created",dn
-
-    def notify_modified(self, dn):
-        print "modified",dn
-
-    def notify_deleted(self, dn):
-        print "deleted",dn
+    def get_short_dn(self, dn):
+        return dn.lower().replace(',' + self.srcbasedn.lower(), '')
+
+    def notify_created(self, dn, ok=True):
+        if ok:
+            logger.debug('{} created'.format(self.get_short_dn(dn)))
+            self.result['add']['ok'].append(dn)
+        else:
+            self.logger.warning("failed to add {}".format(dn))
+            self.result['add']['failed'].append(dn)
+
+    def notify_modified(self, dn, ok=True):
+        if ok:
+            logger.debug('{} modified'.format(self.get_short_dn(dn)))
+            self.result['update']['ok'].append(dn)
+        else:
+            self.logger.error("failed to modify {}".format(dn))
+            self.result['update']['failed'].append(dn)
+
+    def notify_deleted(self, dn, ok=True):
+        if ok:
+            logger.debug('{} deleted'.format(self.get_short_dn(dn)))
+            self.result['delete']['ok'].append(dn)
+        else:
+            self.logger.error("failed to delete {}".format(dn))
+            self.result['delete']['failed'].append(dn)
 
     def notify_renamed(self, dn, newdn, uid, newuid, options):
-        print "renamed",dn,newdn
-        subprocess.check_call("%s %s %s %s %s" % (options.renamecommand,dn,newdn,uid,newuid),shell=True)
+        print "renamed", dn, newdn
+        subprocess.check_call(
+            "%s %s %s %s %s" % (options.renamecommand, dn, newdn, uid, newuid),
+            shell=True)
+
+
+
+class SyncReplConsumer(ReconnectLDAPObject, SyncreplConsumer):
+    """
+    Syncrepl Consumer interface
+    """
+
+    def __init__(self, dest, syncrepl_entry_callback, *args, **kwargs):
+        self.logger = logging.getLogger()
+        # Initialise the LDAP Connection first
+        ldap.ldapobject.ReconnectLDAPObject.__init__(self, *args, **kwargs)
+        # We need this for later internal use
+        self.__presentUUIDs = dict()
+        self.cookie = None
+        self.dest_ldap = dest
+        self.syncrepl_entry_callback = syncrepl_entry_callback
+
+    def syncrepl_get_cookie(self):
+        return self.cookie
+
+    def syncrepl_set_cookie(self, cookie):
+        self.cookie = cookie
+
+    def syncrepl_entry(self, dn, attributes, uuid):
+        # First we determine the type of change we have here
+        # (and store away the previous data for later if needed)
+        if uuid in self.__presentUUIDs:
+            change_type = 'modify'
+        else:
+            change_type = 'add'
+        # Now we store our knowledge of the existence of this entry
+        self.__presentUUIDs[uuid] = dn
+        # Debugging
+        logger.debug('{}: {} ({})'.format(dn, change_type, ",".join(attributes.keys())))
+        # If we have a cookie then this is not our first time being run,
+        # so it must be a change
+        if self.cookie is not None:
+            self.syncrepl_entry_callback(dn, attributes)
+
+
+    def syncrepl_delete(self, uuids):
+        """ syncrepl_delete """
+        # Make sure we know about the UUID being deleted, just in case...
+        uuids = [uuid for uuid in uuids if uuid in self.__presentUUIDs]
+        # Delete all the UUID values we know of
+        for uuid in uuids:
+            logger.debug('detected deletion of entry {} ({})', uuid, self.__presentUUIDs[uuid])
+            del self.__presentUUIDs[uuid]
+
+    def syncrepl_present(self, uuids, refreshDeletes=False):
+        """ called on initial sync """
+        if uuids is not None:
+            self.logger.debug('uuids: {}'.format(','.join(uuids)))
+        # If we have not been given any UUID values,
+        # then we have recieved all the present controls...
+        if uuids is None:
+            # We only do things if refreshDeletes is false as the syncrepl
+            # extension will call syncrepl_delete instead when it detects a
+            # delete notice
+            if not refreshDeletes:
+                deletedEntries = [
+                    uuid for uuid in self.__presentUUIDs
+                ]
+                self.syncrepl_delete(deletedEntries)
+            # Phase is now completed, reset the list
+            self.__presentUUIDs = {}
+        else:
+            # Note down all the UUIDs we have been sent
+            for uuid in uuids:
+                self.__presentUUIDs[uuid] = True
+
+
+    def syncrepl_refreshdone(self):
+        self.logger.info('Initial synchronization is now done, persist phase begins')
+        #self.logger.debug('UUIDs:\n' + '\n'.join(self.__presentUUIDs))
+
+
+
+class LdapSyncRepl(LdapSync):
+    def __init__(self, destsrv,
+                 destadmindn, destadminpw,
+                 basedn, destbasedn,
+                 options=Options(), source_ldap_url_obj=None):
+        # Install our signal handlers
+        signal.signal(signal.SIGTERM, self.shutdown)
+        self.watcher_running = False
+        self.source_ldap_url_obj = source_ldap_url_obj
+        self.ldap_credentials = False
+        self.source_ldap_connection = None
+        super(LdapSyncRepl, self).__init__(destsrv,
+                                           destadmindn, destadminpw,
+                                           basedn, destbasedn, options)
+
+
+    def sync(self):
+        self._dest_ldap_connect()
+        self.watcher_running = True
+        while self.watcher_running:
+            self.logger.info('Connecting to source LDAP server')
+            # Prepare the LDAP server connection (triggers the connection as well)
+            self.source_ldap_connection = SyncReplConsumer(self.con,
+                                                           self.perform_application_sync_callback,
+                                                           self.source_ldap_url_obj.initializeUrl())
+
+            if self.source_ldap_url_obj.who and self.source_ldap_url_obj.cred:
+                self.ldap_credentials = True
+                # Now we login to the LDAP server
+                try:
+                    self.source_ldap_connection.simple_bind_s(
+                        self.source_ldap_url_obj.who, self.source_ldap_url_obj.cred)
+                except ldap.INVALID_CREDENTIALS, e:
+                    print 'Login to LDAP server failed: ', str(e)
+                    sys.exit(1)
+                except ldap.SERVER_DOWN:
+                    print 'LDAP server is down, going to retry.'
+                    time.sleep(5)
+                    continue
+
+            # Commence the syncing
+            self.logger.info('Staring sync process')
+            ldap_search = self.source_ldap_connection.syncrepl_search(
+                self.source_ldap_url_obj.dn or '',
+                self.source_ldap_url_obj.scope or ldap.SCOPE_SUBTREE,
+                mode='refreshAndPersist',
+                attrlist=self.source_ldap_url_obj.attrs,
+                filterstr=self.source_ldap_url_obj.filterstr or '(objectClass=*)'
+            )
+
+            try:
+                while self.source_ldap_connection.syncrepl_poll(all=1, msgid=ldap_search):
+                    print ".",
+            except KeyboardInterrupt:
+                # User asked to exit
+                print "aborted\n"
+                self.shutdown(None, None)
+            except Exception, e:
+                # Handle any exception
+                if self.watcher_running:
+                    self.logger.exception('Encountered a problem, going to retry.')
+                    time.sleep(5)
+
+    def perform_application_sync_callback(self, dn, attributes):
+        logger.debug('{}: src: {}'.format(dn, str(attributes)))
+        try:
+            self._syncLdapObject(dn, attributes)
+        except ldap.NO_SUCH_OBJECT:
+            self.logger.info("SKIPPED: {} object does not exist on target".format(dn))
+            return False
+        return True
+
+    def shutdown(self, signum, stack):
+        # Declare the needed global variables
+        self.logger.info('Shutting down!')
+
+        # We are no longer running
+        self.watcher_running = False
 
 
@@ -362,37 +571,36 @@
     logger = logging.getLogger()
 
-    args=getArguments()
+    args = getArguments()
     if args.debug:
         logger.setLevel(logging.DEBUG)
-    conffile=args.configfile
+    conffile = args.configfile
 
     exclude = None
 
-    config=ConfigParserDefaults()
+    config = ConfigParserDefaults()
     config.read(conffile)
 
     srcfile = None
     try:
-        srcfile = config.get("source","file")
+        srcfile = config.get("source", "file")
     except:
         pass
 
-    basedn = config.get("source","baseDn")
-    filter = config.get0("source","filter", None)
-    attrs = config.get0("source", "attributes", None)
-
-    if srcfile==None:
-        srv = config.get("source","server") 
-        admindn = config.get("source","bindDn") 
-        adminpw = config.get("source","bindPassword")
-        starttls = config.getboolean("source","starttls")
-
-    destsrv = config.get("destination","server") 
-    destadmindn = config.get("destination","bindDn") 
-    destadminpw = config.get("destination","bindPassword")
-    destbasedn = config.get("destination","baseDn")
-    destdelete = config.getboolean("destination","delete")
+    basedn = config.get("source", "baseDn")
+    filterstr = config.get0("source", "filter", None)
+
+    if srcfile is None:
+        srv = config.get("source", "server")
+        admindn = config.get("source", "bindDn")
+        adminpw = config.get("source", "bindPassword")
+        starttls = config.getboolean("source", "starttls")
+
+    destsrv = config.get("destination", "server")
+    destadmindn = config.get("destination", "bindDn")
+    destadminpw = config.get("destination", "bindPassword")
+    destbasedn = config.get("destination", "baseDn")
+    destdelete = config.getboolean("destination", "delete")
     try:
-        rdn = config.get("destination","rdn")
+        rdn = config.get("destination", "rdn")
         logger.warning("setting rdn is currently ignored")
     except:
@@ -401,26 +609,36 @@
     options = Options()
     try:
-        options.exclude = config.get("destination","excludesubtree").lower()
+        options.exclude = config.get("destination", "excludesubtree").lower()
     except:
         pass
 
-    options.updateonly = not config.getboolean("destination","create", False)
-    options.starttls = config.getboolean("destination","starttls", False)
-    options.renameattr = config.get0("destination","detectRename", None)
-    options.renamecommand = config.get0("destination","detectRename", None)
-    options.pwd_max_days = int(config.get("source","pwd_max_days",0))
-    options.filter = filter
-
-    
+    options.updateonly = not config.getboolean("destination", "create", False)
+    options.starttls = config.getboolean("destination", "starttls", False)
+    options.renameattr = config.get0("destination", "detectRename", None)
+    options.renamecommand = config.get0("destination", "detectRename", None)
+    options.pwd_max_days = int(config.get("source", "pwd_max_days", 0))
+    options.filter = filterstr
+
+    # Set source.attrlist as global option.
+    # If source would use less attributes than dest,
+    # all attributes not retrieved from source would be deleted from dest
     try:
-        options.attrfilter = config.get("destination","attributes").split(",")
+        options.attrlist = config.get("source", "attributes").split(",")
     except:
-        options.attrfilter = None
-
-    if srcfile:
-        objects = readLDIFSource(srcfile)
+        options.attrlist = None
+
+    if config.get0('source', 'mode') == 'syncrepl':
+        ldapsync = LdapSyncRepl(
+            destsrv, destadmindn, destadminpw, basedn, destbasedn,
+            options,
+            source_ldap_url_obj=config.get_ldap_url_obj('source'))
+        ldapsync.sync()
     else:
-        objects = readLdapSource(srv,admindn,adminpw,basedn,filter,attrs,starttls)
-
-    ldapsync = LdapSync(destsrv,destadmindn,destadminpw,basedn,destbasedn,options)
-    ldapsync.sync(objects)
+        if srcfile:
+            objects = readLDIFSource(srcfile)
+        else:
+            objects = readLdapSource(srv, admindn, adminpw,
+                                     basedn, filterstr, options.attrlist, starttls)
+
+        ldapsync = LdapSync(destsrv, destadmindn, destadminpw, basedn, destbasedn, options)
+        ldapsync.sync(objects)
