1#!/usr/bin/env python 2""" 3This script implements a syncrepl consumer which syncs data from an OpenLDAP 4server to a local (shelve) database. 5 6Notes: 7 8The bound user needs read access to the attributes entryDN and entryCSN. 9""" 10 11# Import modules from Python standard lib 12import logging 13import shelve 14import signal 15import sys 16import time 17 18# Import the python-ldap modules 19import ldap 20import ldapurl 21# Import specific classes from python-ldap 22from ldap.ldapobject import ReconnectLDAPObject 23from ldap.syncrepl import SyncreplConsumer 24 25logger = logging.getLogger('syncrepl') 26logger.setLevel(logging.DEBUG) 27logger.addHandler(logging.StreamHandler()) 28 29# Global state 30watcher_running = True 31ldap_connection = False 32 33 34class SyncReplClient(ReconnectLDAPObject, SyncreplConsumer): 35 """ 36 Syncrepl Consumer Client 37 """ 38 39 def __init__(self, db_path, *args, **kwargs): 40 # Initialise the LDAP Connection first 41 ldap.ldapobject.ReconnectLDAPObject.__init__(self, *args, **kwargs) 42 # Now prepare the data store 43 if db_path: 44 self.__data = shelve.open(db_path, 'c') 45 else: 46 self.__data = {} 47 # We need this for later internal use 48 self.__presentUUIDs = {} 49 50 def close_db(self): 51 # Close the data store properly to avoid corruption 52 self.__data.close() 53 54 def syncrepl_get_cookie(self): 55 if 'cookie' in self.__data: 56 return self.__data['cookie'] 57 58 def syncrepl_set_cookie(self,cookie): 59 self.__data['cookie'] = cookie 60 61 def syncrepl_entry(self, dn, attributes, uuid): 62 logger.debug('dn=%r attributes=%r uuid=%r', dn, attributes, uuid) 63 # First we determine the type of change we have here 64 # (and store away the previous data for later if needed) 65 previous_attributes = {} 66 if uuid in self.__data: 67 change_type = 'modify' 68 previous_attributes = self.__data[uuid] 69 else: 70 change_type = 'add' 71 # Now we store our knowledge of the existence of this entry 72 # (including the DN as an attribute for convenience) 73 attributes['dn'] = dn 74 self.__data[uuid] = attributes 75 # Debugging 76 logger.debug('Detected %s of entry %r', change_type, dn) 77 # If we have a cookie then this is not our first time being run, 78 # so it must be a change 79 if 'ldap_cookie' in self.__data: 80 self.perform_application_sync(dn, attributes, previous_attributes) 81 82 def syncrepl_delete(self,uuids): 83 # Make sure we know about the UUID being deleted, just in case... 84 uuids = [uuid for uuid in uuids if uuid in self.__data] 85 # Delete all the UUID values we know of 86 for uuid in uuids: 87 logger.debug('Detected deletion of entry %r', self.__data[uuid]['dn']) 88 del self.__data[uuid] 89 90 def syncrepl_present(self,uuids,refreshDeletes=False): 91 # If we have not been given any UUID values, 92 # then we have recieved all the present controls... 93 if uuids is None: 94 # We only do things if refreshDeletes is false as the syncrepl 95 # extension will call syncrepl_delete instead when it detects a 96 # delete notice 97 if refreshDeletes is False: 98 deletedEntries = [ 99 uuid 100 for uuid in self.__data.keys() 101 if uuid not in self.__presentUUIDs and uuid != 'ldap_cookie' 102 ] 103 self.syncrepl_delete( deletedEntries ) 104 # Phase is now completed, reset the list 105 self.__presentUUIDs = {} 106 else: 107 # Note down all the UUIDs we have been sent 108 for uuid in uuids: 109 self.__presentUUIDs[uuid] = True 110 111 def syncrepl_refreshdone(self): 112 logger.info('Initial synchronization is now done, persist phase begins') 113 114 def perform_application_sync(self,dn,attributes,previous_attributes): 115 logger.info('Performing application sync for %r', dn) 116 return True 117 118 119# Shutdown handler 120def commenceShutdown(signum, stack): 121 # Declare the needed global variables 122 global watcher_running, ldap_connection 123 logger.warn('Shutting down!') 124 125 # We are no longer running 126 watcher_running = False 127 128 # Tear down the server connection 129 if ldap_connection: 130 ldap_connection.close_db() 131 ldap_connection.unbind_s() 132 del ldap_connection 133 134 # Shutdown 135 sys.exit(0) 136 137# Time to actually begin execution 138# Install our signal handlers 139signal.signal(signal.SIGTERM, commenceShutdown) 140signal.signal(signal.SIGINT, commenceShutdown) 141 142 143try: 144 ldap_url = ldapurl.LDAPUrl(sys.argv[1]) 145 database_path = sys.argv[2] 146except IndexError,e: 147 print ( 148 'Usage:\n' 149 '{script_name} <LDAP URL> <pathname of database>\n' 150 '{script_name} "ldap://127.0.0.1/cn=users,dc=test' 151 '?*' 152 '?sub' 153 '?(objectClass=*)' 154 '?bindname=uid=admin%2ccn=users%2cdc=test,' 155 'X-BINDPW=password" db.shelve' 156 ).format(script_name=sys.argv[0]) 157 sys.exit(1) 158except ValueError as e: 159 print('Error parsing command-line arguments:',str(e)) 160 sys.exit(1) 161 162while watcher_running: 163 logger.info('Connecting to %s now...', ldap_url.initializeUrl()) 164 # Prepare the LDAP server connection (triggers the connection as well) 165 ldap_connection = SyncReplClient(database_path, ldap_url.initializeUrl()) 166 167 # Now we login to the LDAP server 168 try: 169 ldap_connection.simple_bind_s(ldap_url.who, ldap_url.cred) 170 except ldap.INVALID_CREDENTIALS as err: 171 logger.error('Login to LDAP server failed: %s', err) 172 sys.exit(1) 173 except ldap.SERVER_DOWN: 174 logger.warn('LDAP server is down, going to retry.') 175 time.sleep(5) 176 continue 177 178 # Commence the syncing 179 logger.debug('Commencing sync process') 180 ldap_search = ldap_connection.syncrepl_search( 181 ldap_url.dn or '', 182 ldap_url.scope or ldap.SCOPE_SUBTREE, 183 mode = 'refreshAndPersist', 184 attrlist=ldap_url.attrs, 185 filterstr = ldap_url.filterstr or '(objectClass=*)' 186 ) 187 188 try: 189 while ldap_connection.syncrepl_poll( all = 1, msgid = ldap_search): 190 pass 191 except KeyboardInterrupt: 192 # User asked to exit 193 commenceShutdown(None, None) 194 except Exception as err: 195 # Handle any exception 196 if watcher_running: 197 logger.exception('Unhandled exception, going to retry: %s', err) 198 logger.info('Going to retry after 5 secs') 199 time.sleep(5) 200