Changeset 1221


Ignore:
Timestamp:
11/16/16 16:18:48 (5 years ago)
Author:
joergs
Message:

added syncrepl

Location:
dassldapsync
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • dassldapsync/dassldapsync.conf

    r1219 r1221  
    1212#starttls=true
    1313#pwd_max_days=0
     14#mode=syncrepl
    1415
    1516[destination]
     
    1819bindPassword=secret
    1920baseDn=dc=example,dc=com
    20 #attributes=
    2121delete=true
    2222create=true
  • dassldapsync/dassldapsync.py

    r1220 r1221  
    22# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
    33
     4# core modules
    45import argparse
    56import ConfigParser
     7import logging
     8from   pprint import pprint
     9import signal
     10import subprocess
     11import sys
     12import time
     13
     14# external modules
    615import datetime
    716import dateutil.parser
    817import dateutil.tz
    918import ldap
     19from   ldap.ldapobject import ReconnectLDAPObject
    1020import ldap.modlist
     21from   ldap.syncrepl import SyncreplConsumer
     22import ldapurl
    1123import ldif
    12 import logging
    13 import os
    14 import sys
     24
     25
    1526
    1627def getArguments():
    1728    configfile = '/etc/dassldapsync.conf'
    18     parser = argparse.ArgumentParser(description='Synchronize the content of two LDAP servers.' )
     29    parser = argparse.ArgumentParser(description='Synchronize the content of two LDAP servers.')
    1930    parser.add_argument('-d', '--debug', action='store_true', help="enable debug output")
    20     parser.add_argument('configfile', default=configfile, help="Configuration file [default: {}]".format(configfile))
    21     args = parser.parse_args()
    22     return args
    23 
    24 
    25 class Options:
     31    parser.add_argument('configfile', default=configfile,
     32                        help="Configuration file [default: {}]".format(configfile))
     33    return parser.parse_args()
     34
     35
     36class Options(object):
    2637    def __init__(self):
    27         self.delete=True
    28         self.starttls=False
    29         self.updateonly=False
    30         self.filter=None
    31         self.attrfilter=None
    32         self.exclude=None
    33         self.renameattr=None
    34         self.renamecommand=None
    35         self.pwd_max_days=0
    36        
     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
    3748class ConfigParserDefaults(ConfigParser.ConfigParser, object):
    38     def get(self, section, option, default = None):
     49    def get(self, section, option, default=None):
    3950        try:
    4051            result = super(self.__class__, self).get(section, option)
     
    4657        return result
    4758
    48     def get0(self, section, option, default = None):
     59    def get_section(self, section):
     60        if section in self._sections:
     61            return self._sections[section]
     62
     63    def get0(self, section, option, default=None):
    4964        try:
    5065            result = super(self.__class__, self).get(section, option)
     
    5368        return result
    5469
    55     def getboolean(self, section, option, default = None):
     70    def getboolean(self, section, option, default=None):
    5671        try:
    5772            result = super(self.__class__, self).getboolean(section, option)
     
    6378        return result
    6479
     80    def get_ldap_url_obj(self, section):
     81        baseurl = 'ldap://{server}:389/{basedn}'.format(**(self.get_section(section)))
     82        attrs = None
     83        if self.get0(section, 'attributes') is not None:
     84            attrs = self.get(section, 'attributes').split(',')
     85        return ldapurl.LDAPUrl(
     86            baseurl,
     87            dn=self.get(section, 'baseDn', ''),
     88            who=self.get0(section, 'bindDn'),
     89            cred=self.get0(section, 'basePassword'),
     90            filterstr=self.get0(section, 'filter'),
     91            attrs=attrs
     92        )
     93
     94
    6595def readLDIFSource(path):
    6696    logger = logging.getLogger()
    6797    logger.info("reading LDAP objects from file {}".format(path))
    68     with open(path,'r') as f:
     98    with open(path, 'r') as f:
    6999        parser = ldif.LDIFRecordList(f)
    70100        parser.parse()
     
    72102    return result
    73103
    74 def readLdapSource(server,binddn,bindpw,basedn,filter,attrs=None,starttls=False):
     104def readLdapSource(server, binddn, bindpw, basedn, filterstr, attrlist=None, starttls=False):
    75105    logger = logging.getLogger()
    76106    logger.info("reading LDAP objects from server {}".format(server))
    77     con = ldap.open(server,port=389)
     107    con = ldap.open(server, port=389)
    78108    if starttls:
    79         con.start_tls_s()
    80     con.simple_bind_s(binddn,bindpw)
    81     results=con.search_s(basedn,ldap.SCOPE_SUBTREE,filter,attrs)
     109        con.start_tls_s()
     110    con.simple_bind_s(binddn, bindpw)
     111    results = con.search_s(basedn, ldap.SCOPE_SUBTREE, filterstr, attrlist)
    82112    return results
    83113
    84114class LdapSync(object):
    85     def __init__(self, destserver,destbinddn,destbindpw,srcbasedn,destbasedn,options=Options()):
     115    def __init__(self, destserver,
     116                 destbinddn, destbindpw,
     117                 srcbasedn, destbasedn, options=Options()):
    86118        self.logger = logging.getLogger()
    87        
     119
    88120        self.destserver = destserver
    89121        self.destbasedn = destbasedn
     
    91123        self.destbindpw = destbindpw
    92124        self.options = options
    93        
     125
    94126        self.srcbasedn = srcbasedn
    95127
    96128        self.con = None
    97        
    98         self.attrmap=ldap.cidict.cidict({})
    99         self.classmap={}
    100 
    101         self.junk_attrs = [ "memberof", "modifiersname", "modifytimestamp", "entryuuid", "entrycsn", "contextcsn", "creatorsname", "createtimestamp", "structuralobjectclass", "pwdchangedtime", "pwdfailuretime" ]
    102 
    103     def __ldap_connect(self):
     129
     130        self.attrmap = ldap.cidict.cidict({})
     131        self.classmap = {}
     132
     133        self.junk_attrs = ["memberof", "modifiersname", "modifytimestamp", "entryuuid",
     134                           "entrycsn", "contextcsn", "creatorsname", "createtimestamp",
     135                           "structuralobjectclass", "pwdchangedtime", "pwdfailuretime"]
     136
     137        self.reset_result()
     138
     139
     140    def reset_result(self):
     141        self.result = {
     142            'add': {'ok': [], 'failed': []},
     143            'update': {'ok': [], 'failed': []},
     144            'delete': {'ok': [], 'failed': []},
     145        }
     146
     147
     148    def _dest_ldap_connect(self):
    104149        if self.con is None:
    105150            self.logger.info("connect to destination LDAP server {}".format(self.destserver))
    106             self.con = ldap.open(self.destserver,port=389)
     151            self.con = ldap.open(self.destserver, port=389)
    107152            if self.options.starttls:
    108153                self.con.start_tls_s()
    109             self.con.simple_bind_s(self.destbinddn,self.destbindpw)
     154            self.con.simple_bind_s(self.destbinddn, self.destbindpw)
    110155
    111156    def __adapt_dn(self, dn):
     
    113158        if self.srcbasedn != self.destbasedn:
    114159            dn_old = dn
    115             rpath = dn[:-len(srcbasedn)]
    116             dn=rpath+self.destbasedn
     160            rpath = dn[:-len(self.srcbasedn)]
     161            dn = rpath+self.destbasedn
    117162            self.logger.debug("moved {} to {}".format(dn_old, dn))
    118163            # print "dn:",dn,"src:",srcbasedn,"rpath:",rpath,"dest:",destbasedn
     
    132177        self.logger.debug("modifying LDAP objects retrieved from source LDAP")
    133178
    134         update_objects=[]
    135        
     179        update_objects = []
     180
    136181        for r in searchresult:
    137182            dn = self.__adapt_dn(r[0])
    138             d=ldap.cidict.cidict(r[1])
     183            d = ldap.cidict.cidict(r[1])
    139184
    140185            if self.__is_dn_included(dn):
    141                 objectclasses=d["objectclass"]
    142 
    143                 newObjectclasses=[]
     186                objectclasses = d["objectclass"]
     187
     188                newObjectclasses = []
    144189                for o in objectclasses:
    145190                    if o.lower() in self.classmap:
    146                             new_oc = self.classmap[o.lower()]
    147                             if not new_oc in newObjectclasses:
    148                                 newObjectclasses.append(new_oc)
     191                        new_oc = self.classmap[o.lower()]
     192                        if new_oc not in newObjectclasses:
     193                            newObjectclasses.append(new_oc)
    149194                    else:
    150                         #pass
    151                         if not o in newObjectclasses:
     195                        if o not in newObjectclasses:
    152196                            newObjectclasses.append(o)
    153197
    154                 d["objectclass"]=newObjectclasses
     198                d["objectclass"] = newObjectclasses
    155199
    156200                for a in d.keys():
    157                     attr=a
     201                    attr = a
    158202                    if self.attrmap.has_key(a.lower()):
    159                         attr=self.attrmap[attr].lower()
    160                         if attr.lower()!=a.lower():
    161                             # print "# ",a," -> ",attr
    162                             values=d[a]
     203                        attr = self.attrmap[attr].lower()
     204                        if attr.lower() != a.lower():
     205                            values = d[a]
    163206                            del d[a]
    164                             d[attr]=values
    165 
    166                 update_objects.append((dn,d))
     207                            d[attr] = values
     208
     209                update_objects.append((dn, d))
    167210        return update_objects
    168211
    169212
    170     def __get_dest_entry(self, dn, entry):
     213    def _get_dest_entry(self, dn, entry):
    171214        """
    172215        In the destination LDAP, the objects should be named
    173216        according to options.renameattr.
    174217        """
     218        attrlist = self.options.attrlist
     219
    175220        existingDestDn = None
    176221        existingDestEntry = None
    177222        if self.options.renameattr and entry.has_key(self.options.renameattr):
    178             searchresult = self.con.search_s(self.destbasedn,ldap.SCOPE_SUB,"%s=%s" % (self.options.renameattr,entry[self.options.renameattr][0]))
    179             if searchresult != None and len(searchresult)>0:
     223            searchresult = self.con.search_s(
     224                self.destbasedn,
     225                ldap.SCOPE_SUBTREE,
     226                '%s=%s' % (self.options.renameattr, entry[self.options.renameattr][0]), attrlist)
     227            if searchresult is not None and len(searchresult) > 0:
    180228                existingDestDn, existingDestEntry = searchresult[0]
    181229                if existingDestDn.lower() != dn.lower():
    182                     self.con.modrdn_s(existingDestDn,dn)
    183                     notify_renamed(existingDestDn, dn, existingDestEntry[self.options.renameattr][0],entry[self.options.renameattr][0],options)
     230                    self.con.modrdn_s(existingDestDn, dn)
     231                    self.notify_renamed(existingDestDn, dn,
     232                                        existingDestEntry[self.options.renameattr][0],
     233                                        entry[self.options.renameattr][0],
     234                                        options)
    184235        if existingDestDn is None:
    185             searchresult=self.con.search_s(dn,ldap.SCOPE_BASE,"objectclass=*")
     236            searchresult = self.con.search_s(dn, ldap.SCOPE_BASE, 'objectclass=*', attrlist)
    186237            existingDestDn, existingDestEntry = searchresult[0]
    187238        return (existingDestDn, existingDestEntry)
     
    191242        # hack for syncing accounts locked by password policy
    192243        do_unlock = False
    193         if self.options.pwd_max_days>0 and entry.has_key('pwdChangedTime'):
     244        if self.options.pwd_max_days > 0 and entry.has_key('pwdChangedTime'):
    194245            # print "pwdChangedTime set for",dn
    195246            pwdChange = entry['pwdChangedTime'][0]
    196247            d = dateutil.parser.parse(pwdChange)
    197             if (now-d)>max_age:
    198                 entry['pwdAccountLockedTime']=['000001010000Z']
    199                 self.logger.info( "locking {} {}".format(dn, pwdChange))
     248            if (now-d) > max_age:
     249                entry['pwdAccountLockedTime'] = ['000001010000Z']
     250                self.logger.info("locking {} {}".format(dn, pwdChange))
    200251            else:
    201252                # pwdAccountLockedTime is a operational attribute,
    202253                # and therefore not part of entry.
    203254                # Do extra search to retrieve attribute.
    204                 searchresult = self.con.search_s(dn,ldap.SCOPE_BASE,"objectclass=*", \
    205                 attrlist = ['pwdAccountLockedTime'])
     255                searchresult = self.con.search_s(
     256                    dn, ldap.SCOPE_BASE,
     257                    "objectclass=*", attrlist=['pwdAccountLockedTime'])
    206258                tmp_dn, tmp_entry = searchresult[0]
    207259                if tmp_entry.has_key('pwdAccountLockedTime'):
     
    210262
    211263
    212 
    213     def __syncLdapDestination(self, update_objects):
    214        
    215         result = {
    216             'add': { 'ok': [], 'failed': [] },
    217             'update': { 'ok': [], 'failed': [] },
    218         }
    219 
     264    def _syncLdapObject(self, srcDn, srcAttributes):
    220265        tzutc = dateutil.tz.gettz('UTC')
    221266        now = datetime.datetime.now(tzutc)
    222267        max_age = datetime.timedelta(days=self.options.pwd_max_days)
    223268
     269        try:
     270            destDn, destAttributes = self._get_dest_entry(srcDn, srcAttributes)
     271
     272            # hack for syncing accounts locked by password policy
     273            do_unlock = self.__handle_pwdAccountLockedTime(srcDn, srcAttributes, now, max_age)
     274
     275            mod_attrs = ldap.modlist.modifyModlist(destAttributes, srcAttributes)
     276
     277            # hack for unlocking, see above
     278            if do_unlock:
     279                self.logger.info("unlocking {} {}".format(destDn, 'pwdAccountLockedTime'))
     280                mod_attrs.append((ldap.MOD_DELETE, 'pwdAccountLockedTime', None))
     281
     282            if self.options.attrlist is not None:
     283                mod_attrs = [a for a in mod_attrs if a[1].lower() in self.options.attrlist]
     284
     285            if self.junk_attrs is not None:
     286                mod_attrs = [a for a in mod_attrs if a[1].lower() not in self.junk_attrs]
     287
     288            if mod_attrs:
     289                try:
     290                    self.logger.debug('mod_attrs: ' + str(mod_attrs))
     291                    self.con.modify_s(srcDn, mod_attrs)
     292                    self.notify_modified(srcDn)
     293                except:
     294                    self.logger.exception('modify failed')
     295                    self.notify_modified(srcDn, False)
     296
     297        except ldap.NO_SUCH_OBJECT:
     298            if not self.options.updateonly:
     299                try:
     300                    self.con.add_s(srcDn, ldap.modlist.addModlist(srcAttributes, self.junk_attrs))
     301                    self.notify_created(srcDn)
     302                except (ldap.OBJECT_CLASS_VIOLATION,
     303                        ldap.NO_SUCH_OBJECT,
     304                        ldap.CONSTRAINT_VIOLATION):
     305                    self.notify_created(srcDn, False)
     306
     307
     308    def __syncLdapDestination(self, update_objects):
     309
    224310        logger.debug("writing data to destination LDAP")
    225         for o in update_objects:
    226             dn,entry=o
    227             #logger.debug(dn)
    228             try:
    229                 destDn,destEntry=self.__get_dest_entry(dn, entry)
    230 
    231                 # hack for syncing accounts locked by password policy
    232                 do_unlock = self.__handle_pwdAccountLockedTime(dn, entry, now, max_age)
    233    
    234                 mod_attrs=ldap.modlist.modifyModlist(destEntry,entry)
    235 
    236                 # hack for unlocking, see above
    237                 if do_unlock:
    238                     self.logger.info( "unlocking {} {}".format(dn,pwdChange))
    239                     mod_attrs.append( (ldap.MOD_DELETE,'pwdAccountLockedTime',None) )
    240 
    241                 if self.options.attrfilter is not None:
    242                     mod_attrs=[ a for a in mod_attrs if a[1].lower() in options.attrfilter]
    243 
    244                 if self.junk_attrs is not None:
    245                     mod_attrs=[ a for a in mod_attrs if a[1].lower() not in self.junk_attrs]
    246 
    247                 if mod_attrs:
    248                     try:
    249                             self.con.modify_s(dn,mod_attrs)
    250                             result['update']['ok'].append(dn)
    251                             self.notify_modified(dn)
    252                     except:
    253                             self.logger.error("failed to modify {} ({})".format(dn,mod_attrs))
    254                             result['update']['failed'].append(dn)
    255 
    256             except ldap.NO_SUCH_OBJECT:
    257                 if options.updateonly==True:
    258                     continue
    259 
    260                 try:
    261                     self.con.add_s(dn,ldap.modlist.addModlist(entry,self.junk_attrs))
    262                     self.notify_created(dn)
    263                     result['add']['ok'].append(dn)
    264                 except (ldap.OBJECT_CLASS_VIOLATION, ldap.NO_SUCH_OBJECT, ldap.CONSTRAINT_VIOLATION):
    265                     self.logger.warning("failed to add {}".format(dn))
    266                     result['add']['failed'].append(dn)
    267         return result
    268 
    269                    
     311        for obj in update_objects:
     312            dn, entry = obj
     313            self._syncLdapObject(dn, entry)
     314
     315
    270316    def __deleteDestLdapObjects(self, update_objects):
    271317        """
     
    275321        """
    276322
    277         result = {
    278             'delete': { 'ok': [], 'failed': [] },
    279         }
    280 
    281         searchresult=self.con.search_s(self.destbasedn,ldap.SCOPE_SUBTREE,self.options.filter)
    282         existing=[ x[0].lower() for x in searchresult ]
    283 
    284         morituri=existing
     323        searchresult = self.con.search_s(self.destbasedn, ldap.SCOPE_SUBTREE, self.options.filter)
     324        existing = [x[0].lower() for x in searchresult]
     325
     326        morituri = existing
    285327
    286328        if self.destbasedn.lower() in existing:
    287329            morituri.remove(self.destbasedn.lower())
    288330
    289         for o in update_objects:
    290             dn,entry=o
     331        for obj in update_objects:
     332            dn, entry = obj
    291333            if dn.lower() in existing:
    292334                morituri.remove(dn.lower())
     
    295337                try:
    296338                    self.con.delete_s(dn)
    297                     self.notify_deleted(dn)
    298                     result['delete']['ok'].append(dn)
     339                    self.notify_deleted(dn)
    299340                except:
    300                     self.logger.error("failed to delete {}".format(dn))
    301                     result['delete']['failed'].append(dn)
    302         return result
    303                    
     341                    self.notify_deleted(dn, False)
     342
    304343
    305344    def sync(self, searchresult):
     
    307346        Synchronize entries from searchresult to destination LDAP server.
    308347        """
    309         result = {
    310             'add': { 'ok': [], 'failed': [] },
    311             'update': { 'ok': [], 'failed': [] },
    312             'delete': { 'ok': [], 'failed': [] },
    313         }
    314 
    315         if len(searchresult)==0:
     348        if len(searchresult) == 0:
    316349            self.logger.error("empty source, aborting")
    317350            return
    318351
    319         self.__ldap_connect()
     352        self._dest_ldap_connect()
    320353
    321354        update_objects = self.__adapt_source_ldap_objects(searchresult)
    322         result = self.__syncLdapDestination(update_objects)
    323         if self.options.delete==True and self.options.updateonly==False:
    324             result.update(self.__deleteDestLdapObjects(update_objects))
     355        self.__syncLdapDestination(update_objects)
     356        if self.options.delete and not self.options.updateonly:
     357            self.__deleteDestLdapObjects(update_objects)
    325358        self.con.unbind()
    326  
    327         self.__log_summary(result, True)
    328 
    329 
    330 
    331     def __log_summary(self, result, show_failed = True, show_ok = False):
     359
     360        self.__log_summary(True)
     361
     362
     363    def __log_summary(self, show_failed=True, show_ok=False):
     364        result = self.result
    332365        for action in result.keys():
    333366            ok = len(result[action]['ok'])
     
    343376                print "\n".join(result[action]['failed'])
    344377
    345 
    346     def notify_created(self, dn):
    347         print "created",dn
    348 
    349     def notify_modified(self, dn):
    350         print "modified",dn
    351 
    352     def notify_deleted(self, dn):
    353         print "deleted",dn
     378    def get_short_dn(self, dn):
     379        return dn.lower().replace(',' + self.srcbasedn.lower(), '')
     380
     381    def notify_created(self, dn, ok=True):
     382        if ok:
     383            logger.debug('{} created'.format(self.get_short_dn(dn)))
     384            self.result['add']['ok'].append(dn)
     385        else:
     386            self.logger.warning("failed to add {}".format(dn))
     387            self.result['add']['failed'].append(dn)
     388
     389    def notify_modified(self, dn, ok=True):
     390        if ok:
     391            logger.debug('{} modified'.format(self.get_short_dn(dn)))
     392            self.result['update']['ok'].append(dn)
     393        else:
     394            self.logger.error("failed to modify {}".format(dn))
     395            self.result['update']['failed'].append(dn)
     396
     397    def notify_deleted(self, dn, ok=True):
     398        if ok:
     399            logger.debug('{} deleted'.format(self.get_short_dn(dn)))
     400            self.result['delete']['ok'].append(dn)
     401        else:
     402            self.logger.error("failed to delete {}".format(dn))
     403            self.result['delete']['failed'].append(dn)
    354404
    355405    def notify_renamed(self, dn, newdn, uid, newuid, options):
    356         print "renamed",dn,newdn
    357         subprocess.check_call("%s %s %s %s %s" % (options.renamecommand,dn,newdn,uid,newuid),shell=True)
     406        print "renamed", dn, newdn
     407        subprocess.check_call(
     408            "%s %s %s %s %s" % (options.renamecommand, dn, newdn, uid, newuid),
     409            shell=True)
     410
     411
     412
     413class SyncReplConsumer(ReconnectLDAPObject, SyncreplConsumer):
     414    """
     415    Syncrepl Consumer interface
     416    """
     417
     418    def __init__(self, dest, syncrepl_entry_callback, *args, **kwargs):
     419        self.logger = logging.getLogger()
     420        # Initialise the LDAP Connection first
     421        ldap.ldapobject.ReconnectLDAPObject.__init__(self, *args, **kwargs)
     422        # We need this for later internal use
     423        self.__presentUUIDs = dict()
     424        self.cookie = None
     425        self.dest_ldap = dest
     426        self.syncrepl_entry_callback = syncrepl_entry_callback
     427
     428    def syncrepl_get_cookie(self):
     429        return self.cookie
     430
     431    def syncrepl_set_cookie(self, cookie):
     432        self.cookie = cookie
     433
     434    def syncrepl_entry(self, dn, attributes, uuid):
     435        # First we determine the type of change we have here
     436        # (and store away the previous data for later if needed)
     437        if uuid in self.__presentUUIDs:
     438            change_type = 'modify'
     439        else:
     440            change_type = 'add'
     441        # Now we store our knowledge of the existence of this entry
     442        self.__presentUUIDs[uuid] = dn
     443        # Debugging
     444        logger.debug('{}: {} ({})'.format(dn, change_type, ",".join(attributes.keys())))
     445        # If we have a cookie then this is not our first time being run,
     446        # so it must be a change
     447        if self.cookie is not None:
     448            self.syncrepl_entry_callback(dn, attributes)
     449
     450
     451    def syncrepl_delete(self, uuids):
     452        """ syncrepl_delete """
     453        # Make sure we know about the UUID being deleted, just in case...
     454        uuids = [uuid for uuid in uuids if uuid in self.__presentUUIDs]
     455        # Delete all the UUID values we know of
     456        for uuid in uuids:
     457            logger.debug('detected deletion of entry {} ({})', uuid, self.__presentUUIDs[uuid])
     458            del self.__presentUUIDs[uuid]
     459
     460    def syncrepl_present(self, uuids, refreshDeletes=False):
     461        """ called on initial sync """
     462        if uuids is not None:
     463            self.logger.debug('uuids: {}'.format(','.join(uuids)))
     464        # If we have not been given any UUID values,
     465        # then we have recieved all the present controls...
     466        if uuids is None:
     467            # We only do things if refreshDeletes is false as the syncrepl
     468            # extension will call syncrepl_delete instead when it detects a
     469            # delete notice
     470            if not refreshDeletes:
     471                deletedEntries = [
     472                    uuid for uuid in self.__presentUUIDs
     473                ]
     474                self.syncrepl_delete(deletedEntries)
     475            # Phase is now completed, reset the list
     476            self.__presentUUIDs = {}
     477        else:
     478            # Note down all the UUIDs we have been sent
     479            for uuid in uuids:
     480                self.__presentUUIDs[uuid] = True
     481
     482
     483    def syncrepl_refreshdone(self):
     484        self.logger.info('Initial synchronization is now done, persist phase begins')
     485        #self.logger.debug('UUIDs:\n' + '\n'.join(self.__presentUUIDs))
     486
     487
     488
     489class LdapSyncRepl(LdapSync):
     490    def __init__(self, destsrv,
     491                 destadmindn, destadminpw,
     492                 basedn, destbasedn,
     493                 options=Options(), source_ldap_url_obj=None):
     494        # Install our signal handlers
     495        signal.signal(signal.SIGTERM, self.shutdown)
     496        self.watcher_running = False
     497        self.source_ldap_url_obj = source_ldap_url_obj
     498        self.ldap_credentials = False
     499        self.source_ldap_connection = None
     500        super(LdapSyncRepl, self).__init__(destsrv,
     501                                           destadmindn, destadminpw,
     502                                           basedn, destbasedn, options)
     503
     504
     505    def sync(self):
     506        self._dest_ldap_connect()
     507        self.watcher_running = True
     508        while self.watcher_running:
     509            self.logger.info('Connecting to source LDAP server')
     510            # Prepare the LDAP server connection (triggers the connection as well)
     511            self.source_ldap_connection = SyncReplConsumer(self.con,
     512                                                           self.perform_application_sync_callback,
     513                                                           self.source_ldap_url_obj.initializeUrl())
     514
     515            if self.source_ldap_url_obj.who and self.source_ldap_url_obj.cred:
     516                self.ldap_credentials = True
     517                # Now we login to the LDAP server
     518                try:
     519                    self.source_ldap_connection.simple_bind_s(
     520                        self.source_ldap_url_obj.who, self.source_ldap_url_obj.cred)
     521                except ldap.INVALID_CREDENTIALS, e:
     522                    print 'Login to LDAP server failed: ', str(e)
     523                    sys.exit(1)
     524                except ldap.SERVER_DOWN:
     525                    print 'LDAP server is down, going to retry.'
     526                    time.sleep(5)
     527                    continue
     528
     529            # Commence the syncing
     530            self.logger.info('Staring sync process')
     531            ldap_search = self.source_ldap_connection.syncrepl_search(
     532                self.source_ldap_url_obj.dn or '',
     533                self.source_ldap_url_obj.scope or ldap.SCOPE_SUBTREE,
     534                mode='refreshAndPersist',
     535                attrlist=self.source_ldap_url_obj.attrs,
     536                filterstr=self.source_ldap_url_obj.filterstr or '(objectClass=*)'
     537            )
     538
     539            try:
     540                while self.source_ldap_connection.syncrepl_poll(all=1, msgid=ldap_search):
     541                    print ".",
     542            except KeyboardInterrupt:
     543                # User asked to exit
     544                print "aborted\n"
     545                self.shutdown(None, None)
     546            except Exception, e:
     547                # Handle any exception
     548                if self.watcher_running:
     549                    self.logger.exception('Encountered a problem, going to retry.')
     550                    time.sleep(5)
     551
     552    def perform_application_sync_callback(self, dn, attributes):
     553        logger.debug('{}: src: {}'.format(dn, str(attributes)))
     554        try:
     555            self._syncLdapObject(dn, attributes)
     556        except ldap.NO_SUCH_OBJECT:
     557            self.logger.info("SKIPPED: {} object does not exist on target".format(dn))
     558            return False
     559        return True
     560
     561    def shutdown(self, signum, stack):
     562        # Declare the needed global variables
     563        self.logger.info('Shutting down!')
     564
     565        # We are no longer running
     566        self.watcher_running = False
    358567
    359568
     
    362571    logger = logging.getLogger()
    363572
    364     args=getArguments()
     573    args = getArguments()
    365574    if args.debug:
    366575        logger.setLevel(logging.DEBUG)
    367     conffile=args.configfile
     576    conffile = args.configfile
    368577
    369578    exclude = None
    370579
    371     config=ConfigParserDefaults()
     580    config = ConfigParserDefaults()
    372581    config.read(conffile)
    373582
    374583    srcfile = None
    375584    try:
    376         srcfile = config.get("source","file")
     585        srcfile = config.get("source", "file")
    377586    except:
    378587        pass
    379588
    380     basedn = config.get("source","baseDn")
    381     filter = config.get0("source","filter", None)
    382     attrs = config.get0("source", "attributes", None)
    383 
    384     if srcfile==None:
    385         srv = config.get("source","server")
    386         admindn = config.get("source","bindDn")
    387         adminpw = config.get("source","bindPassword")
    388         starttls = config.getboolean("source","starttls")
    389 
    390     destsrv = config.get("destination","server")
    391     destadmindn = config.get("destination","bindDn")
    392     destadminpw = config.get("destination","bindPassword")
    393     destbasedn = config.get("destination","baseDn")
    394     destdelete = config.getboolean("destination","delete")
     589    basedn = config.get("source", "baseDn")
     590    filterstr = config.get0("source", "filter", None)
     591
     592    if srcfile is None:
     593        srv = config.get("source", "server")
     594        admindn = config.get("source", "bindDn")
     595        adminpw = config.get("source", "bindPassword")
     596        starttls = config.getboolean("source", "starttls")
     597
     598    destsrv = config.get("destination", "server")
     599    destadmindn = config.get("destination", "bindDn")
     600    destadminpw = config.get("destination", "bindPassword")
     601    destbasedn = config.get("destination", "baseDn")
     602    destdelete = config.getboolean("destination", "delete")
    395603    try:
    396         rdn = config.get("destination","rdn")
     604        rdn = config.get("destination", "rdn")
    397605        logger.warning("setting rdn is currently ignored")
    398606    except:
     
    401609    options = Options()
    402610    try:
    403         options.exclude = config.get("destination","excludesubtree").lower()
     611        options.exclude = config.get("destination", "excludesubtree").lower()
    404612    except:
    405613        pass
    406614
    407     options.updateonly = not config.getboolean("destination","create", False)
    408     options.starttls = config.getboolean("destination","starttls", False)
    409     options.renameattr = config.get0("destination","detectRename", None)
    410     options.renamecommand = config.get0("destination","detectRename", None)
    411     options.pwd_max_days = int(config.get("source","pwd_max_days",0))
    412     options.filter = filter
    413 
    414    
     615    options.updateonly = not config.getboolean("destination", "create", False)
     616    options.starttls = config.getboolean("destination", "starttls", False)
     617    options.renameattr = config.get0("destination", "detectRename", None)
     618    options.renamecommand = config.get0("destination", "detectRename", None)
     619    options.pwd_max_days = int(config.get("source", "pwd_max_days", 0))
     620    options.filter = filterstr
     621
     622    # Set source.attrlist as global option.
     623    # If source would use less attributes than dest,
     624    # all attributes not retrieved from source would be deleted from dest
    415625    try:
    416         options.attrfilter = config.get("destination","attributes").split(",")
     626        options.attrlist = config.get("source", "attributes").split(",")
    417627    except:
    418         options.attrfilter = None
    419 
    420     if srcfile:
    421         objects = readLDIFSource(srcfile)
     628        options.attrlist = None
     629
     630    if config.get0('source', 'mode') == 'syncrepl':
     631        ldapsync = LdapSyncRepl(
     632            destsrv, destadmindn, destadminpw, basedn, destbasedn,
     633            options,
     634            source_ldap_url_obj=config.get_ldap_url_obj('source'))
     635        ldapsync.sync()
    422636    else:
    423         objects = readLdapSource(srv,admindn,adminpw,basedn,filter,attrs,starttls)
    424 
    425     ldapsync = LdapSync(destsrv,destadmindn,destadminpw,basedn,destbasedn,options)
    426     ldapsync.sync(objects)
     637        if srcfile:
     638            objects = readLDIFSource(srcfile)
     639        else:
     640            objects = readLdapSource(srv, admindn, adminpw,
     641                                     basedn, filterstr, options.attrlist, starttls)
     642
     643        ldapsync = LdapSync(destsrv, destadmindn, destadminpw, basedn, destbasedn, options)
     644        ldapsync.sync(objects)
Note: See TracChangeset for help on using the changeset viewer.