Source code for syncrepl_client.callbacks

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim: ts=4 sw=4 et

# syncrepl_client callback code.
#
# Refer to the AUTHORS file for copyright statements.
#
# This file is made available under the terms of the BSD 3-Clause License,
# the text of which may be found in the file `LICENSE.md` that was included
# with this distribution, and also at
# https://github.com/akkornel/syncrepl/blob/master/LICENSE.md 
#
# The Python docstrings contained in this file are also made available under the terms
# of the Creative Commons Attribution-ShareAlike 4.0 International Public License,
# the text of which may be found in the file `LICENSE_others.md` that was included
# with this distribution, and also at
# https://github.com/akkornel/syncrepl/blob/master/LICENSE_others.md
#


# Python 2 support
from __future__ import print_function

from sys import stdout


[docs]class BaseCallback(object): """ :class:`BaseCallback` is a class containing all of the callbacks which :class:`syncrepl_client.Syncrepl` may call. It is implemented as a new-style class, with class methods (although :class:`~syncrepl_client.Syncrepl` doesn't care). This class exists for two reasons: * It documents each callback, the parameters the callback receives, and what the callback means. * It gives you a useful base class to sink unwanted callbacks. If you only care about certain callbacks, you can implement them in your class, and let all the other callbacks percolate up to this class, where they are received and ignored. Another reason for using classes is because they can be stacked. For example, if you want to handle callbacks, but you also want them to be logged, you can have your callback class use :class:`~syncrepl_client.callbacks.LoggingCallback` as a base class, and then set the :class:`~syncrepl_client.callbacks.LoggingCallback`'s :attr:`~syncrepl_client.callbacks.LoggingCallback.dest` attribute to the log sink (which could be :obj:`~sys.stdout`, or another file handle). .. note:: If your class needs any kind of setup or initalization before it can receive callbacks, it is suggested that you implement your callbacks as instance methods, and then use normal class instantiation to do your preparation, before handing off the instance to :class:`~syncrepl_client.Syncrepl`'s constructor. """ @classmethod
[docs] def bind_complete(cls, ldap, cursor): """Called to mark a successful bind to the LDAP server. :param ldap.LDAPObject ldap: The LDAP object. :param sqlite3.Cursor cursor: A mid-transaction database cursor. :return: None - any returned value is ignored. This callback is used to indicate a successful bind, and to give you one opportunity to interact with the LDAP server, before the connection is taken over by the syncrepl search. For example, if you want to record the bind DN (which you might not know, if you are doing things like a SASL bind), or you want to retrieve the schema stored on the server, this is the time to do it! This is the very first callback to be called. Once the callback completes, the syncrepl search will begin, and other callbacks will start coming in. .. note:: Once the callback has completed, there is no guarantee that `ldap` will still be a valid reference. .. warning:: If you start any asynchronous operations (which includes searches), those operations **must** be completed before this callback returns. .. warning:: Please do not unbind `ldap` from the LDAP server! .. warning:: Once the callback completes, this LDAP connection will be used for the syncrepl search. **No other operations will be allowed!** If you want to communicate with the LDAP server after this callback completes, you will need to set up a separate LDAP connection. `cursor` provides access to the underlying database, as described in :class:`~syncrepl_client.DBInterface`. If you are storing your own data in syncrepl_client's database, you can use this cursor to make appropriate changes to *your own* tables. That will ensure your database changes are saved at the same time as ours! .. warning:: Do not commit the in-progress transaction! The commit will take place automatically, once your callback returns. """ pass
@classmethod
[docs] def refresh_done(cls, items, cursor): """Called to mark the end of the refresh phase. :param dict items: The items currently in the directory. :param sqlite3.Cursor cursor: A mid-transaction database cursor. :return: None -- any returned value is ignored. When receiving this callback, you know that the refresh phase has completed, and your view of the directory is now consistent with the LDAP server (at least, the part of the directory which matches your search and your access). :data:`items` is a dictionary (or, an object which behaves like a dictionary) where the keys are DNs and the values are dicts of attributes & their values. Every item in :data:`items` is present in the directory at the time this callback started. .. note:: :data:`items` is read-only. Any attempt to add, change, or delete and item will cause an :obj:`AttributeError` to be raised. .. warning:: The content :data:`items` is guaranteed to be consistent *only inside this callback*. Once this callback returns, any attempts to access :data:`items` again will result in undefined behavior. `cursor` provides access to the underlying database, as described in :class:`~syncrepl_client.DBInterface`. If you are storing your own data in syncrepl_client's database, you can use this cursor to make appropriate changes to *your own* tables. That will ensure your database changes are saved at the same time as ours! .. warning:: Do not commit the in-progress transaction! The commit will take place automatically, once your callback returns. If you need to do any sort of synchronization with anyone else, this is the best time to do it. Once you return from this callback, the persist phase will begin. The first database commit will also take place. .. note:: Once you return from this callback, it may be some time before you see anything happen (either another callback, or :meth:`syncrepl_client.Syncrepl.poll` returning. That is because the first database commit is taking place, and there may be _many_ changes to commit! If you are operating in refresh-only mode, then as soon as this callback completes, :meth:`syncrepl_client.Syncrepl.poll` will return :obj:`False`. It is then safe to call :meth:`~syncrepl_client.Syncrepl.unbind`. """ pass
@classmethod
[docs] def record_add(cls, dn, attrs, cursor): """Called to indicate the addition of a new LDAP record. :param str dn: The DN of the added record. :param attrs: The record's attributes. :type attrs: Dict of lists of bytes :param sqlite3.Cursor cursor: A mid-transaction database cursor. :return: None - any returned value is ignored. .. warning:: :data:`attrs` is passed by reference. If you modify the dictionary—or its contents—in any way, you will pay for it later! This callback can happen in all modes, and in all phases, to indicate that an entry has been added to your view of the search results. In refresh-only mode, and the refresh phase of refresh-and-perist mode, the addition may have taken place at any time since your last update. In the persist phase of refresh-and-persist mode, a new entry has just been added—or modified—such that it matches your search. Attributes which are not present in the record will not present in `attrs`. Dict keys are attribute names, and dict values are arrays (to support multi-valued attributes). .. note:: Just because the dict values are arrays, does not mean that all attributes are multi-valued. The LDAP client does not know which attributes are single- and which are multi-valued, so it assumes that all are multi-valued. Also, all attribute values will come in as (in Python 2) :obj:`str` objects or (in Python 3) :obj:`bytes` objects. To learn which attributes are single- or multi-valued, and to learn the type (or, in LDAP terms, the *syntax*) of an attribute, you need to look at the schema, possibly using :mod:`ldap.schema`. `cursor` provides access to the underlying database, as described in :class:`~syncrepl_client.DBInterface`. If you are storing your own data in syncrepl_client's database, you can use this cursor to make appropriate changes to *your own* tables. That will ensure your database changes are saved at the same time as ours! .. warning:: Do not commit the in-progress transaction! The commit will take place automatically, once your callback returns. """ pass
@classmethod
[docs] def record_delete(cls, dn, cursor): """Called to indicate the deletion of an LDAP record. :param str dn: The DN of the deleted record. :param sqlite3.Cursor cursor: A mid-transaction database cursor. :return: None - any returned value is ignored. This callback can happen in all modes, and in all phases, to indicate that an entry has been either been deleted, or that it no longer matches your search. In refresh-only mode, and the refresh phase of refresh-and-perist mode, the deletion may have taken place at any time since your last update. In the persist phase of refresh-and-persist mode, the entry has just disappeared. `cursor` provides access to the underlying database, as described in :class:`~syncrepl_client.DBInterface`. If you are storing your own data in syncrepl_client's database, you can use this cursor to make appropriate changes to *your own* tables. That will ensure your database changes are saved at the same time as ours! .. warning:: Do not commit the in-progress transaction! The commit will take place automatically, once your callback returns. """ pass
@classmethod
[docs] def record_rename(cls, old_dn, new_dn, cursor): """Called to indicate a change in DN. :param str old_dn: The old DN. :param str new_dn: The new DN. :param sqlite3.Cursor cursor: A mid-transaction database cursor. :return: None - any returned value is ignored. This callback happens when an entry's DN changes. This callback can happen in all modes, and in all phases, to indicate that an entry's DN has been changed. In refresh-only mode, and the refresh phase of refresh-and-perist mode, the change may have taken place at any time since your last update. In the persist phase of refresh-and-persist mode, the entry has just changed. `cursor` provides access to the underlying database, as described in :class:`~syncrepl_client.DBInterface`. If you are storing your own data in syncrepl_client's database, you can use this cursor to make appropriate changes to *your own* tables. That will ensure your database changes are saved at the same time as ours! .. warning:: Do not commit the in-progress transaction! The commit will take place automatically, once your callback returns. You should expect a call to :meth:`~syncrepl_client.callbacks.BaseCallback.record_change()` shortly after this callback completes. """ pass
@classmethod
[docs] def record_change(cls, dn, old_attrs, new_attrs, cursor): """Called to indicate a change in attributes. :param str dn: The DN of the changed record. :param old_attrs: The old attributes. :type old_attrs: Dict of lists of bytes :param new_attrs: The new attributes. :type new_attrs: Dict of lists of bytes :param sqlite3.Cursor cursor: A mid-transaction database cursor. :return: None - any returned value is ignored. This callback happens when an entry has changed. You are provided with the old attributes, and the new attributes. It is up to you to determine what the changes are (if you care). .. note:: If you look back at :meth:`~syncrepl_client.callbacks.BaseCallback.record_add`, see the note about changing `attrs`, and how it will come back to bite you? Well, here's where it comes back to bite you! .. warning:: `new_attrs` is passed by reference. If you modify the dictionary—or its contents—in any way; you will pay for it later! This callback can happen in all modes, and in all phases, to indicate that an entry has been changed. In refresh-only mode, and the refresh phase of refresh-and-perist mode, the change may have taken place at any time since your last update. In the persist phase of refresh-and-persist mode, the entry has just changed. .. note:: Just because the dict values are arrays, does not mean that all attributes are multi-valued. The LDAP client does not know which attributes are single- and which are multi-valued, so it assumes that all are multi-valued. Also, all attribute values will come in as (in Python 2) :obj:`str` objects or (in Python 3) :obj:`bytes` objects. To learn which attributes are single- or multi-valued, and to learn the type (or, in LDAP terms, the *syntax*) of an attribute, you need to look at the schema, possibly using :mod:`ldap.schema`. `cursor` provides access to the underlying database, as described in :class:`~syncrepl_client.DBInterface`. If you are storing your own data in syncrepl_client's database, you can use this cursor to make appropriate changes to *your own* tables. That will ensure your database changes are saved at the same time as ours! .. warning:: Do not commit the in-progress transaction! The commit will take place automatically, once your callback returns. """ pass
@classmethod
[docs] def cookie_change(cls, cookie): """Called to log a change in Syncrepl cookie. :param str cookie: The new Syncrepl cookie. :return None - any returned value is ignored. This callback happens any time the LDAP server sends us a new Syncrepl cookie. The Syncrepl cookie is an opaque string, which we send to the LDAP server at the start of a Syncrepl search. If we do not have one, then the LDAP server knows to send us everything. If we *do* have one, then the LDAP server can use that to know how far behind we are, and to send us just the changes. This callback can happen in all modes, and in all phases, as it is up to the LDAP server to give us a new Syncrepl cookie, when it is appropriate to do so. """ pass
@classmethod
[docs] def debug(cls, message): """Called to log debug messages. :param str message: A message of some sort. :return: None - any returned value is ignored. This method doesn't have much of a use. It's just a way for :class:`~syncrepl_client.Syncrepl` to log debug messages. There's no guarantee that you'll get anything meaningful, or anything at all. The safest thing to do is to just `pass` this method. Or, subclass :class:`~syncrepl_client.callbacks.BaseCallback`. """ pass
[docs]class LoggingCallback(BaseCallback): """ :class:`~syncrepl_client.callbacks.LoggingCallback` is a callback class which logs each callback. It is useful for debugging purposes, as the output is not meant to be machine-readable. Each callback will cause messages to be printed to the file set in :attr:`~syncrepl_client.callbacks.LoggingCallback.dest`. For the :meth:`~syncrepl_client.callbacks.BaseCallback.bind_complete` callback, the bind DN is printed. For callbacks containing DNs, the DNs are printed. For callbacks containing attribute dictionaries, each dictionary's contents are printed. For a list of callbacks, and what they mean, see :class:`~syncrepl_client.callbacks.BaseCallback`. """ dest = stdout """The log destination. This can be anything which can be used in :func:`print`'s `file` parameter. Defaults to :obj:`sys.stdout`. """ @classmethod def bind_complete(cls, ldap, cursor): print('BIND COMPLETE!', file=cls.dest) print("\tWE ARE:", ldap.whoami_s(), file=cls.dest) @classmethod def refresh_done(cls, items, cursor): print('REFRESH COMPLETE!', file=cls.dest) print('BEGIN DIRECTORY CONTENTS:', file=cls.dest) for item in items: print(item, file=cls.dest) attrs = items[item] for attr in attrs.keys(): print("\t", attr, sep='', file=cls.dest) for value in attrs[attr]: print("\t\t", value, sep='', file=cls.dest) print('END DIRECTORY CONTENTS', file=cls.dest) @classmethod def record_add(cls, dn, attrs, cursor): print('NEW RECORD:', dn, file=cls.dest) for attr in attrs.keys(): print("\t", attr, sep='', file=cls.dest) for value in attrs[attr]: print("\t\t", value, sep='', file=cls.dest) @classmethod def record_delete(cls, dn, cursor): print('DELETED RECORD:', dn, file=cls.dest) @classmethod def record_rename(cls, old_dn, new_dn, cursor): print('RENAMED RECORD:', file=cls.dest) print("\tOld:", old_dn, file=cls.dest) print("\tNew:", new_dn, file=cls.dest) @classmethod def record_change(cls, dn, old_attrs, new_attrs, cursor): print('RECORD CHANGED:', dn, file=cls.dest) for attr in new_attrs.keys(): print("\t", attr, sep='', file=cls.dest) for value in new_attrs[attr]: print("\t\t", value, sep='', file=cls.dest) @classmethod def cookie_change(cls, cookie): print('COOKIE CHANGED:', cookie) @classmethod def debug(cls, message): print('[DEBUG]', message, file=cls.dest)