#!/usr/bin/python3 """ This script implements a syncrepl consumer which syncs data from an OpenLDAP server to a local (shelve) database. Notes: The bound user needs read access to the attributes entryDN and entryCSN. """ # Import modules from Python standard lib import logging import shelve import signal import sys import time # Import the python-ldap modules import ldap import ldapurl # Import specific classes from python-ldap from ldap.ldapobject import ReconnectLDAPObject from ldap.syncrepl import SyncreplConsumer logger = logging.getLogger('syncrepl') logger.setLevel(logging.DEBUG) logger.addHandler(logging.StreamHandler()) # Global state watcher_running = True ldap_connection = False class SyncReplClient(ReconnectLDAPObject, SyncreplConsumer): """ Syncrepl Consumer Client """ def __init__(self, db_path, *args, **kwargs): # Initialise the LDAP Connection first ldap.ldapobject.ReconnectLDAPObject.__init__(self, *args, **kwargs) # Now prepare the data store if db_path: self.__data = shelve.open(db_path, 'c') else: self.__data = {} # We need this for later internal use self.__presentUUIDs = {} def close_db(self): # Close the data store properly to avoid corruption self.__data.close() def syncrepl_get_cookie(self): if 'cookie' in self.__data: return self.__data['cookie'] def syncrepl_set_cookie(self,cookie): self.__data['cookie'] = cookie def syncrepl_entry(self, dn, attributes, uuid): logger.debug('dn=%r attributes=%r uuid=%r', dn, attributes, uuid) # First we determine the type of change we have here # (and store away the previous data for later if needed) previous_attributes = {} if uuid in self.__data: change_type = 'modify' previous_attributes = self.__data[uuid] else: change_type = 'add' # Now we store our knowledge of the existence of this entry # (including the DN as an attribute for convenience) attributes['dn'] = dn self.__data[uuid] = attributes # Debugging logger.debug('Detected %s of entry %r', change_type, dn) # If we have a cookie then this is not our first time being run, # so it must be a change if 'ldap_cookie' in self.__data: self.perform_application_sync(dn, attributes, previous_attributes) def syncrepl_delete(self,uuids): # Make sure we know about the UUID being deleted, just in case... uuids = [uuid for uuid in uuids if uuid in self.__data] # Delete all the UUID values we know of for uuid in uuids: logger.debug('Detected deletion of entry %r', self.__data[uuid]['dn']) del self.__data[uuid] def syncrepl_present(self,uuids,refreshDeletes=False): # 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 refreshDeletes is False: deletedEntries = [ uuid for uuid in self.__data.keys() if uuid not in self.__presentUUIDs and uuid != 'ldap_cookie' ] 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): logger.info('Initial synchronization is now done, persist phase begins') def perform_application_sync(self,dn,attributes,previous_attributes): logger.info('Performing application sync for %r', dn) return True # Shutdown handler def commenceShutdown(signum, stack): # Declare the needed global variables global watcher_running, ldap_connection logger.warn('Shutting down!') # We are no longer running watcher_running = False # Tear down the server connection if ldap_connection: ldap_connection.close_db() ldap_connection.unbind_s() del ldap_connection # Shutdown sys.exit(0) # Time to actually begin execution # Install our signal handlers signal.signal(signal.SIGTERM, commenceShutdown) signal.signal(signal.SIGINT, commenceShutdown) try: ldap_url = ldapurl.LDAPUrl(sys.argv[1]) database_path = sys.argv[2] except IndexError,e: print ( 'Usage:\n' '{script_name} \n' '{script_name} "ldap://127.0.0.1/cn=users,dc=test' '?*' '?sub' '?(objectClass=*)' '?bindname=uid=admin%2ccn=users%2cdc=test,' 'X-BINDPW=password" db.shelve' ).format(script_name=sys.argv[0]) sys.exit(1) except ValueError as e: print('Error parsing command-line arguments:',str(e)) sys.exit(1) while watcher_running: logger.info('Connecting to %s now...', ldap_url.initializeUrl()) # Prepare the LDAP server connection (triggers the connection as well) ldap_connection = SyncReplClient(database_path, ldap_url.initializeUrl()) # Now we login to the LDAP server try: ldap_connection.simple_bind_s(ldap_url.who, ldap_url.cred) except ldap.INVALID_CREDENTIALS as err: logger.error('Login to LDAP server failed: %s', err) sys.exit(1) except ldap.SERVER_DOWN: logger.warn('LDAP server is down, going to retry.') time.sleep(5) continue # Commence the syncing logger.debug('Commencing sync process') ldap_search = ldap_connection.syncrepl_search( ldap_url.dn or '', ldap_url.scope or ldap.SCOPE_SUBTREE, mode = 'refreshAndPersist', attrlist=ldap_url.attrs, filterstr = ldap_url.filterstr or '(objectClass=*)' ) try: while ldap_connection.syncrepl_poll( all = 1, msgid = ldap_search): pass except KeyboardInterrupt: # User asked to exit commenceShutdown(None, None) except Exception as err: # Handle any exception if watcher_running: logger.exception('Unhandled exception, going to retry: %s', err) logger.info('Going to retry after 5 secs') time.sleep(5)