File: syncrepl.py

package info (click to toggle)
python-ldap 3.4.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,756 kB
  • sloc: python: 9,558; ansic: 3,052; makefile: 139; sh: 79
file content (199 lines) | stat: -rw-r--r-- 6,597 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#!/usr/bin/env python
"""
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} <LDAP URL> <pathname of database>\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)