ldappool-2.2.0/0000775000175100017510000000000013234420105013343 5ustar zuulzuul00000000000000ldappool-2.2.0/ChangeLog0000664000175100017510000000331113234420104015112 0ustar zuulzuul00000000000000CHANGES ======= 2.2.0 ----- * Updated from global requirements * Avoid tox\_install.sh for constraints support * Updated from global requirements * Updated from global requirements * Updated from global requirements * Turn on warning-is-error for sphinx build * Switch from oslosphinx to openstackdocstheme * Fix html\_last\_updated\_fmt for Python3 2.1.0 ----- * Updated from global requirements * Updated from global requirements * Updated from global requirements * Don't call start\_tls\_s() twice * [Fix gate]Update test requirement * Add Constraints support * update README to reflect actual ldap dependency * Expose SERVER\_DOWN if connection fails * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements 2.0.0 ----- * Add py3 info to setup.cfg * Updated from global requirements * make ldappool py3 compatible * use standard docstring convention for parameters * Use standard-library logging to record errors * Raise an explicit BackendError on TLS failures * Fix pool\_full race condition * additional files to ignore in .gitignore * Fix license in setup.py * add .gitreview and fix ldappool gate * Add test-requirements for py27 testing * PEP8 fixes * Add support for tox unit testing * Initialize conn in \_create\_connector (fixes: #7) * Use setuptools when available * #4: UTF-8 encode passwd only when set * starting 1.1 1.0 --- * raised version * preparing 1.0 * fix use\_tls flag * packaging tweaks * added a MANIFEST template * added a few keywords for pypi indexation * simplified setup * more docs * initial import of server-core's ldappool * first commit ldappool-2.2.0/tox.ini0000666000175100017510000000253613234417673014706 0ustar zuulzuul00000000000000[tox] minversion = 2.0 skipsdist = True envlist = py27,py34,py35,pep8,cover,docs [testenv] usedevelop = True install_command = pip install {opts} {packages} setenv = VIRTUAL_ENV={envdir} OS_STDOUT_NOCAPTURE=False OS_STDERR_NOCAPTURE=False deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = find . -type f -name "*.pyc" -delete python setup.py testr --slowest --testr-args='{posargs}' whitelist_externals = find [testenv:pep8] commands = flake8 [testenv:venv] commands = {posargs} [testenv:cover] commands = python setup.py testr --coverage --testr-args='{posargs}' [flake8] # D100: Missing docstring in public module # D101: Missing docstring in public class # D102: Missing docstring in public method # D103: Missing docstring in public function # D104: Missing docstring in public package # D105: Missing docstring in magic method # D200: One-line docstring should fit on one line with quotes # D210: No whitespaces allowed surrounding docstring text # D401: First line should be in imperative mood ignore = D100,D101,D102,D104,D105,D200,D210,D401 show-source = True exclude = .venv,.tox,dist,doc,*egg,build [testenv:docs] commands= python setup.py build_sphinx ldappool-2.2.0/setup.py0000666000175100017510000000200613234417673015075 0ustar zuulzuul00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools # In python < 2.7.4, a lazy loading of package `pbr` will break # setuptools if some other modules registered functions in `atexit`. # solution from: http://bugs.python.org/issue15881#msg170215 try: import multiprocessing # noqa except ImportError: pass setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) ldappool-2.2.0/CONTRIBUTORS0000666000175100017510000000014113234417673015241 0ustar zuulzuul00000000000000By order of appearance: - Tarek Ziadé - Chris McDonough ldappool-2.2.0/PKG-INFO0000664000175100017510000000646013234420105014446 0ustar zuulzuul00000000000000Metadata-Version: 1.1 Name: ldappool Version: 2.2.0 Summary: A simple connector pool for python-ldap. Home-page: https://git.openstack.org/cgit/openstack/ldappool Author: OpenStack Author-email: UNKNOWN License: UNKNOWN Description-Content-Type: UNKNOWN Description: ldappool ======== A simple connector pool for pyldap. The pool keeps LDAP connectors alive and let you reuse them, drastically reducing the time spent to initiate a ldap connection. The pool has useful features like: - transparent reconnection on failures or server restarts - configurable pool size and connectors timeouts - configurable max lifetime for connectors - a context manager to simplify acquiring and releasing a connector **You need pyldap in order to use this library** Quickstart :::::::::: To work with the pool, you just need to create it, then use it as a context manager with the *connection* method:: from ldappool import ConnectionManager cm = ConnectionManager('ldap://localhost') with cm.connection('uid=adminuser,ou=logins,dc=mozilla', 'password') as conn: .. do something with conn .. The connector returned by *connection* is a LDAPObject, that's binded to the server. See https://github.com/pyldap/pyldap/ for details on how to use a connector. ConnectionManager options ::::::::::::::::::::::::: Here are the options you can use when instanciating the pool: - **uri**: ldap server uri **[mandatory]** - **bind**: default bind that will be used to bind a connector. **default: None** - **passwd**: default password that will be used to bind a connector. **default: None** - **size**: pool size. **default: 10** - **retry_max**: number of attempts when a server is down. **default: 3** - **retry_delay**: delay in seconds before a retry. **default: .1** - **use_tls**: activate TLS when connecting. **default: False** - **timeout**: connector timeout. **default: -1** - **use_pool**: activates the pool. If False, will recreate a connector each time. **default: True** The **connection** method takes two options: - **bind**: bind used to connect. If None, uses the pool default's. **default: None** - **passwd**: password used to connect. If None, uses the pool default's. **default: None** History ::::::: Prior to v2.0.0 ``ldappool`` required ``python-ldap``. As of v2.0.0 this library now required ``pyldap``, a python 3 compatible fork of ``python-ldap``. Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 ldappool-2.2.0/requirements.txt0000666000175100017510000000034713234417673016655 0ustar zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pyldap>=2.4.20 # PSF ldappool-2.2.0/AUTHORS0000664000175100017510000000123013234420104014406 0ustar zuulzuul00000000000000Andreas Jaeger Charles Duffy Charles Duffy Chris McDonough Colleen Murphy Colleen Murphy Lorenzo M. Catucci Monty Taylor Morgan Fainberg Samriddhi Jain Steve Martinelli Tarek Ziade Tarek Ziade Tarek Ziadé Tony Breeds Van Hung Pham pallavi ricolin ldappool-2.2.0/ldappool/0000775000175100017510000000000013234420105015155 5ustar zuulzuul00000000000000ldappool-2.2.0/ldappool/__init__.py0000666000175100017510000003164013234417673017314 0ustar zuulzuul00000000000000# ***** BEGIN LICENSE BLOCK ***** # Version: MPL 1.1/GPL 2.0/LGPL 2.1 # # The contents of this file are subject to the Mozilla Public License Version # 1.1 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License # for the specific language governing rights and limitations under the # License. # # The Original Code is Sync Server # # The Initial Developer of the Original Code is the Mozilla Foundation. # Portions created by the Initial Developer are Copyright (C) 2010 # the Initial Developer. All Rights Reserved. # # Contributor(s): # Tarek Ziade (tarek@mozilla.com) # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), # in which case the provisions of the GPL or the LGPL are applicable instead # of those above. If you wish to allow use of your version of this file only # under the terms of either the GPL or the LGPL, and not to allow others to # use your version of this file under the terms of the MPL, indicate your # decision by deleting the provisions above and replace them with the notice # and other provisions required by the GPL or the LGPL. If you do not delete # the provisions above, a recipient may use your version of this file under # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** """ LDAP Connection Pool. """ import codecs from contextlib import contextmanager import logging from threading import RLock import time import ldap from ldap.ldapobject import ReconnectLDAPObject import six log = logging.getLogger(__name__) _utf8_encoder = codecs.getencoder('utf-8') def utf8_encode(value): """Encode a basestring to UTF-8. If the string is unicode encode it to UTF-8, if the string is str then assume it's already encoded. Otherwise raise a TypeError. :param value: A basestring :returns: UTF-8 encoded version of value :raises TypeError: If value is not basestring """ if isinstance(value, six.text_type): return _utf8_encoder(value)[0] elif isinstance(value, six.binary_type): return value else: raise TypeError("bytes or Unicode expected, got %s" % type(value).__name__) class MaxConnectionReachedError(Exception): pass class BackendError(Exception): def __init__(self, msg, backend): self.bacend = backend Exception.__init__(self, msg) class StateConnector(ReconnectLDAPObject): """Just remembers who is connected, and if connected.""" def __init__(self, *args, **kw): ReconnectLDAPObject.__init__(self, *args, **kw) self.connected = False self.who = '' self.cred = '' self._connection_time = None def get_lifetime(self): """Returns the lifetime of the connection on the server in seconds.""" if self._connection_time is None: return 0 return time.time() - self._connection_time def simple_bind_s(self, who='', cred='', serverctrls=None, clientctrls=None): res = ReconnectLDAPObject.simple_bind_s(self, who, cred, serverctrls, clientctrls) self.connected = True self.who = who self.cred = cred if self._connection_time is None: self._connection_time = time.time() return res def unbind_ext_s(self, serverctrls=None, clientctrls=None): try: return ReconnectLDAPObject.unbind_ext_s(self, serverctrls, clientctrls) finally: self.connected = False self.who = None self.cred = None def add_s(self, *args, **kwargs): return self._apply_method_s(ReconnectLDAPObject.add_s, *args, **kwargs) def modify_s(self, *args, **kwargs): return self._apply_method_s(ReconnectLDAPObject.modify_s, *args, **kwargs) def __str__(self): res = 'LDAP Connector' if self.connected: res += ' (connected)' else: res += ' (disconnected)' if self.who != '': res += ' - who: %r' % self.who if self._uri != '': res += ' - uri: %r' % self._uri return res class ConnectionManager(object): """LDAP Connection Manager. Provides a context manager for LDAP connectors. """ def __init__(self, uri, bind=None, passwd=None, size=10, retry_max=3, retry_delay=.1, use_tls=False, timeout=-1, connector_cls=StateConnector, use_pool=True, max_lifetime=600): self._pool = [] self.size = size self.retry_max = retry_max self.retry_delay = retry_delay self.uri = uri self.bind = bind self.passwd = passwd self._pool_lock = RLock() self.use_tls = use_tls self.timeout = timeout self.connector_cls = connector_cls self.use_pool = use_pool self.max_lifetime = max_lifetime def __len__(self): return len(self._pool) def _match(self, bind, passwd): if passwd is not None: passwd = utf8_encode(passwd) with self._pool_lock: inactives = [] for conn in reversed(self._pool): # already in usage if conn.active: continue # let's check the lifetime if conn.get_lifetime() > self.max_lifetime: # this connector has lived for too long, # we want to unbind it and remove it from the pool try: conn.unbind_s() except Exception: log.debug('Failure attempting to unbind after ' 'timeout; should be harmless', exc_info=True) self._pool.remove(conn) continue # we found a connector for this bind if conn.who == bind and conn.cred == passwd: conn.active = True return conn inactives.append(conn) # no connector was available, let's rebind the latest inactive one if len(inactives) > 0: for conn in inactives: try: self._bind(conn, bind, passwd) return conn except Exception: log.debug('Removing connection from pool after ' 'failure to rebind', exc_info=True) self._pool.remove(conn) return None # There are no connector that match return None def _bind(self, conn, bind, passwd): # let's bind if self.use_tls: try: conn.start_tls_s() except Exception: raise BackendError('Could not activate TLS on established ' 'connection with %s' % self.uri, backend=conn) if bind is not None: conn.simple_bind_s(bind, passwd) conn.active = True def _create_connector(self, bind, passwd): """Creates a connector, binds it, and returns it. :param bind: user login :type bind: string :param passwd: user password :type passwd: string :returns: StateConnector :raises BackendError: If unable to connect to LDAP """ tries = 0 connected = False if passwd is not None: passwd = utf8_encode(passwd) exc = None conn = None # trying retry_max times in a row with a fresh connector while tries < self.retry_max and not connected: try: conn = self.connector_cls(self.uri, retry_max=self.retry_max, retry_delay=self.retry_delay) conn.timeout = self.timeout self._bind(conn, bind, passwd) connected = True except ldap.LDAPError as error: exc = error time.sleep(self.retry_delay) if tries < self.retry_max: log.info('Failure attempting to create and bind ' 'connector; will retry after %r seconds', self.retry_delay, exc_info=True) else: log.error('Failure attempting to create and bind ' 'connector', exc_info=True) tries += 1 if not connected: if isinstance(exc, (ldap.NO_SUCH_OBJECT, ldap.INVALID_CREDENTIALS, ldap.SERVER_DOWN)): raise exc # that's something else raise BackendError(str(exc), backend=conn) return conn def _get_connection(self, bind=None, passwd=None): if bind is None: bind = self.bind if passwd is None: passwd = self.passwd if self.use_pool: # let's try to recycle an existing one conn = self._match(bind, passwd) if conn is not None: return conn # the pool is full if len(self._pool) >= self.size: raise MaxConnectionReachedError(self.uri) # we need to create a new connector conn = self._create_connector(bind, passwd) # adding it to the pool if self.use_pool: with self._pool_lock: self._pool.append(conn) else: # with no pool, the connector is always active conn.active = True return conn def _release_connection(self, connection): if self.use_pool: with self._pool_lock: if not connection.connected: # unconnected connector, let's drop it self._pool.remove(connection) else: # can be reused - let's mark is as not active connection.active = False # done. return else: connection.active = False # let's try to unbind it try: connection.unbind_ext_s() except ldap.LDAPError: # avoid error on invalid state log.debug('Failure attempting to unbind on release; ' 'should be harmless', exc_info=True) @contextmanager def connection(self, bind=None, passwd=None): """Creates a context'ed connector, binds it, and returns it. :param bind: user login :type bind: string :param passwd: user password :type passwd: string :returns: StateConnector :raises MaxConnectionReachedError: If unable to connect to LDAP """ tries = 0 conn = None while tries < self.retry_max: try: conn = self._get_connection(bind, passwd) except MaxConnectionReachedError: tries += 1 time.sleep(0.1) # removing the first inactive connector going backward with self._pool_lock: reversed_list = reversed(list(enumerate(self._pool))) for index, conn_ in reversed_list: if not conn_.active: self._pool.pop(index) break else: break if conn is None: raise MaxConnectionReachedError(self.uri) try: yield conn finally: self._release_connection(conn) def purge(self, bind, passwd=None): """Purge a connector. :param bind: user login :type bind: string :param passwd: user password :type passwd: string """ if self.use_pool: return if passwd is not None: passwd = utf8_encode(passwd) with self._pool_lock: for conn in list(self._pool): if conn.who != bind: continue if passwd is not None and conn.cred == passwd: continue # let's drop it try: conn.unbind_ext_s() except ldap.LDAPError: # invalid state log.debug('Failure attempting to unbind on purge; ' 'should be harmless', exc_info=True) self._pool.remove(conn) ldappool-2.2.0/ldappool/tests/0000775000175100017510000000000013234420105016317 5ustar zuulzuul00000000000000ldappool-2.2.0/ldappool/tests/test_ldapconnection.py0000666000175100017510000001254013234417673022754 0ustar zuulzuul00000000000000# ***** BEGIN LICENSE BLOCK ***** # Version: MPL 1.1/GPL 2.0/LGPL 2.1 # # The contents of this file are subject to the Mozilla Public License Version # 1.1 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License # for the specific language governing rights and limitations under the # License. # # The Original Code is Sync Server # # The Initial Developer of the Original Code is the Mozilla Foundation. # Portions created by the Initial Developer are Copyright (C) 2011 # the Initial Developer. All Rights Reserved. # # Contributor(s): # Tarek Ziade (tarek@mozilla.com) # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), # in which case the provisions of the GPL or the LGPL are applicable instead # of those above. If you wish to allow use of your version of this file only # under the terms of either the GPL or the LGPL, and not to allow others to # use your version of this file under the terms of the MPL, indicate your # decision by deleting the provisions above and replace them with the notice # and other provisions required by the GPL or the LGPL. If you do not delete # the provisions above, a recipient may use your version of this file under # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** import unittest import ldap import ldappool def _bind(self, who='', cred='', **kw): self.connected = True self.who = who self.cred = cred return 1 def _bind_fails(self, who='', cred='', **kw): raise ldap.LDAPError('LDAP connection invalid') def _bind_fails2(self, who='', cred='', **kw): raise ldap.SERVER_DOWN('LDAP connection invalid') def _start_tls_s(self): if self.start_tls_already_called_flag: raise ldap.LOCAL_ERROR else: self.start_tls_already_called_flag = True class TestLDAPConnection(unittest.TestCase): def setUp(self): self.old = ldappool.StateConnector.simple_bind_s ldappool.StateConnector.simple_bind_s = _bind self.old_start_tls_s = ldappool.StateConnector.start_tls_s ldappool.StateConnector.start_tls_s = _start_tls_s ldappool.StateConnector.start_tls_already_called_flag = False def tearDown(self): ldappool.StateConnector.simple_bind_s = self.old ldappool.StateConnector.start_tls_s = self.old_start_tls_s def test_connection(self): uri = '' dn = 'uid=adminuser,ou=logins,dc=mozilla' passwd = 'adminuser' cm = ldappool.ConnectionManager(uri, dn, passwd, use_pool=True, size=2) self.assertEqual(len(cm), 0) with cm.connection('dn', 'pass'): self.assertEqual(len(cm), 1) # if we ask a new one the pool will grow with cm.connection('dn', 'pass'): self.assertEqual(len(cm), 2) # every connector is marked active self.assertTrue(cm._pool[0].active) self.assertTrue(cm._pool[1].active) # if we ask a new one the pool is full try: with cm.connection('dn', 'pass'): pass except ldappool.MaxConnectionReachedError: pass else: raise AssertionError() # down to one active self.assertFalse(cm._pool[1].active) self.assertTrue(cm._pool[0].active) # if we ask a new one the pool is full # but we get the inactive one with cm.connection('dn', 'pass'): self.assertEqual(len(cm), 2) self.assertFalse(cm._pool[1].active) self.assertTrue(cm._pool[0].active) # if we ask a new one the pool is full # but we get the inactive one, and rebind it with cm.connection('dn2', 'pass'): self.assertEqual(len(cm), 2) # the pool is still 2 self.assertEqual(len(cm), 2) # every connector is marked inactive self.assertFalse(cm._pool[0].active) self.assertFalse(cm._pool[1].active) def test_tls_connection(self): uri = '' dn = 'uid=adminuser,ou=logins,dc=mozilla' passwd = 'adminuser' cm = ldappool.ConnectionManager(uri, dn, passwd, use_pool=True, size=2, use_tls=True) with cm.connection(): pass def test_simple_bind_fails(self): unbinds = [] def _unbind(self): unbinds.append(1) # the binding fails with an LDAPError ldappool.StateConnector.simple_bind_s = _bind_fails2 ldappool.StateConnector.unbind_s = _unbind uri = '' dn = 'uid=adminuser,ou=logins,dc=mozilla' passwd = 'adminuser' cm = ldappool.ConnectionManager(uri, dn, passwd, use_pool=True, size=2) self.assertEqual(len(cm), 0) try: with cm.connection('dn', 'pass'): pass except ldap.SERVER_DOWN: pass else: raise AssertionError() ldappool-2.2.0/ldappool/tests/test_ldappool.py0000666000175100017510000001746513234417673021601 0ustar zuulzuul00000000000000# ***** BEGIN LICENSE BLOCK ***** # Version: MPL 1.1/GPL 2.0/LGPL 2.1 # # The contents of this file are subject to the Mozilla Public License Version # 1.1 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License # for the specific language governing rights and limitations under the # License. # # The Original Code is Sync Server # # The Initial Developer of the Original Code is the Mozilla Foundation. # Portions created by the Initial Developer are Copyright (C) 2010 # the Initial Developer. All Rights Reserved. # # Contributor(s): # Tarek Ziade (tarek@mozilla.com) # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), # in which case the provisions of the GPL or the LGPL are applicable instead # of those above. If you wish to allow use of your version of this file only # under the terms of either the GPL or the LGPL, and not to allow others to # use your version of this file under the terms of the MPL, indicate your # decision by deleting the provisions above and replace them with the notice # and other provisions required by the GPL or the LGPL. If you do not delete # the provisions above, a recipient may use your version of this file under # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** import threading import time import unittest import ldap import ldappool # patching StateConnector ldappool.StateConnector.users = { 'uid=tarek,ou=users,dc=mozilla': {'uidNumber': ['1'], 'account-enabled': ['Yes'], 'mail': ['tarek@mozilla.com'], 'cn': ['tarek']}, 'cn=admin,dc=mozilla': {'cn': ['admin'], 'mail': ['admin'], 'uidNumber': ['100']}} def _simple_bind(self, who='', cred='', *args): self.connected = True self.who = who self.cred = cred ldappool.StateConnector.simple_bind_s = _simple_bind def _search(self, dn, *args, **kw): if dn in self.users: return [(dn, self.users[dn])] elif dn == 'ou=users,dc=mozilla': uid = kw['filterstr'].split('=')[-1][:-1] for dn_, value in self.users.items(): if value['uidNumber'][0] != uid: continue return [(dn_, value)] raise ldap.NO_SUCH_OBJECT ldappool.StateConnector.search_s = _search def _add(self, dn, user): self.users[dn] = {} for key, value in user: if not isinstance(value, list): value = [value] self.users[dn][key] = value return ldap.RES_ADD, '' ldappool.StateConnector.add_s = _add def _modify(self, dn, user): if dn in self.users: for type_, key, value in user: if not isinstance(value, list): value = [value] self.users[dn][key] = value return ldap.RES_MODIFY, '' ldappool.StateConnector.modify_s = _modify def _delete(self, dn): if dn in self.users: del self.users[dn] return ldap.RES_DELETE, '' ldappool.StateConnector.delete_s = _delete class LDAPWorker(threading.Thread): def __init__(self, pool): threading.Thread.__init__(self) self.pool = pool self.results = [] def run(self): dn = 'cn=admin,dc=mozilla' for i in range(10): with self.pool.connection() as conn: res = conn.search_s(dn, ldap.SCOPE_BASE, attrlist=['cn']) self.results.append(res) class TestLDAPSQLAuth(unittest.TestCase): def test_ctor_args(self): pool = ldappool.ConnectionManager('ldap://localhost', use_tls=True) self.assertEqual(pool.use_tls, True) def test_pool(self): dn = 'uid=adminuser,ou=logins,dc=mozilla' passwd = 'adminuser' pool = ldappool.ConnectionManager('ldap://localhost', dn, passwd) workers = [LDAPWorker(pool) for i in range(10)] for worker in workers: worker.start() for worker in workers: worker.join() self.assertEqual(len(worker.results), 10) cn = worker.results[0][0][1]['cn'] self.assertEqual(cn, ['admin']) def test_pool_full(self): dn = 'uid=adminuser,ou=logins,dc=mozilla' passwd = 'adminuser' pool = ldappool.ConnectionManager( 'ldap://localhost', dn, passwd, size=1, retry_delay=1., retry_max=5, use_pool=True) class Worker(threading.Thread): def __init__(self, pool, duration): threading.Thread.__init__(self) self.pool = pool self.duration = duration def run(self): with self.pool.connection() as conn: # NOQA time.sleep(self.duration) def tryit(): time.sleep(0.1) with pool.connection() as conn: # NOQA pass # an attempt on a full pool should eventually work # because the connector is reused for i in range(10): tryit() # we have 1 non-active connector now self.assertEqual(len(pool), 1) # an attempt with a full pool should succeed if a # slot gets freed in less than one second. worker1 = Worker(pool, .4) worker1.start() try: tryit() finally: worker1.join() # an attempt with a full pool should fail # if no slot gets freed in less than one second. worker1 = Worker(pool, 1.1) worker1.start() try: self.assertRaises(ldappool.MaxConnectionReachedError, tryit) finally: worker1.join() # we still have one active connector self.assertEqual(len(pool), 1) def test_pool_cleanup(self): dn = 'uid=adminuser,ou=logins,dc=mozilla' passwd = 'adminuser' pool = ldappool.ConnectionManager('ldap://localhost', dn, passwd, size=1, use_pool=True) with pool.connection('bind1') as conn: # NOQA pass with pool.connection('bind2') as conn: # NOQA pass # the second call should have removed the first conn self.assertEqual(len(pool), 1) def test_pool_reuse(self): dn = 'uid=adminuser,ou=logins,dc=mozilla' passwd = 'adminuser' pool = ldappool.ConnectionManager('ldap://localhost', dn, passwd, use_pool=True) with pool.connection() as conn: self.assertTrue(conn.active) self.assertFalse(conn.active) self.assertTrue(conn.connected) with pool.connection() as conn2: pass self.assertTrue(conn is conn2) with pool.connection() as conn: conn.connected = False with pool.connection() as conn2: pass self.assertTrue(conn is not conn2) # same bind and password: reuse with pool.connection('bind', 'passwd') as conn: self.assertTrue(conn.active) self.assertFalse(conn.active) self.assertTrue(conn.connected) with pool.connection('bind', 'passwd') as conn2: pass self.assertTrue(conn is conn2) # same bind different password: rebind ! with pool.connection('bind', 'passwd') as conn: self.assertTrue(conn.active) self.assertFalse(conn.active) self.assertTrue(conn.connected) with pool.connection('bind', 'passwd2') as conn2: pass self.assertTrue(conn is conn2) ldappool-2.2.0/ldappool/tests/__init__.py0000666000175100017510000000000013234417673020440 0ustar zuulzuul00000000000000ldappool-2.2.0/README.rst0000666000175100017510000000413313234417673015055 0ustar zuulzuul00000000000000ldappool ======== A simple connector pool for pyldap. The pool keeps LDAP connectors alive and let you reuse them, drastically reducing the time spent to initiate a ldap connection. The pool has useful features like: - transparent reconnection on failures or server restarts - configurable pool size and connectors timeouts - configurable max lifetime for connectors - a context manager to simplify acquiring and releasing a connector **You need pyldap in order to use this library** Quickstart :::::::::: To work with the pool, you just need to create it, then use it as a context manager with the *connection* method:: from ldappool import ConnectionManager cm = ConnectionManager('ldap://localhost') with cm.connection('uid=adminuser,ou=logins,dc=mozilla', 'password') as conn: .. do something with conn .. The connector returned by *connection* is a LDAPObject, that's binded to the server. See https://github.com/pyldap/pyldap/ for details on how to use a connector. ConnectionManager options ::::::::::::::::::::::::: Here are the options you can use when instanciating the pool: - **uri**: ldap server uri **[mandatory]** - **bind**: default bind that will be used to bind a connector. **default: None** - **passwd**: default password that will be used to bind a connector. **default: None** - **size**: pool size. **default: 10** - **retry_max**: number of attempts when a server is down. **default: 3** - **retry_delay**: delay in seconds before a retry. **default: .1** - **use_tls**: activate TLS when connecting. **default: False** - **timeout**: connector timeout. **default: -1** - **use_pool**: activates the pool. If False, will recreate a connector each time. **default: True** The **connection** method takes two options: - **bind**: bind used to connect. If None, uses the pool default's. **default: None** - **passwd**: password used to connect. If None, uses the pool default's. **default: None** History ::::::: Prior to v2.0.0 ``ldappool`` required ``python-ldap``. As of v2.0.0 this library now required ``pyldap``, a python 3 compatible fork of ``python-ldap``. ldappool-2.2.0/setup.cfg0000666000175100017510000000152313234420105015167 0ustar zuulzuul00000000000000[metadata] name = ldappool summary = A simple connector pool for python-ldap. description-file = README.rst author = OpenStack home-page = https://git.openstack.org/cgit/openstack/ldappool classifier = Intended Audience :: Developers License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 [files] packages = ldappool [build_sphinx] source-dir = doc/source build-dir = doc/build all_files = 1 warning-is-error = 1 [upload_sphinx] upload-dir = doc/build/html [pbr] autodoc_tree_index_modules = True autodoc_tree_excludes = setup.py ldappool/tests/ [wheel] universal = 1 [egg_info] tag_build = tag_date = 0 ldappool-2.2.0/CHANGES.rst0000666000175100017510000000036513234417673015173 0ustar zuulzuul00000000000000Releases :::::::: 1.1 - ? ------- ??? 1.0 - 2012-02-27 ---------------- - Fix ``use_tls`` flag to ConnectionManager; it previously was always set ``False`` no matter what was passed. 0.9 - 2011-10-28 ---------------- - initial release. ldappool-2.2.0/.testr.conf0000666000175100017510000000026713234417673015460 0ustar zuulzuul00000000000000[DEFAULT] test_command=${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./ldappool/tests} $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--listldappool-2.2.0/doc/0000775000175100017510000000000013234420105014110 5ustar zuulzuul00000000000000ldappool-2.2.0/doc/Makefile0000666000175100017510000000614613234417673015601 0ustar zuulzuul00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXSOURCE = source PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SPHINXSOURCE) .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/keystoneauth.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/keystoneauth.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ldappool-2.2.0/doc/source/0000775000175100017510000000000013234420105015410 5ustar zuulzuul00000000000000ldappool-2.2.0/doc/source/conf.py0000666000175100017510000001610513234417673016734 0ustar zuulzuul00000000000000# -*- coding: utf-8 -*- # # ldappool documentation build configuration file, created by # sphinx-quickstart on Sun Dec 6 14:19:25 2009. # # This file is execfile()d with the current directory set to its containing # dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. from __future__ import unicode_literals import os import subprocess import sys import warnings import pbr.version sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.intersphinx', 'openstackdocstheme', ] todo_include_todos = True # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = 'ldappool' copyright = 'OpenStack Contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. version_info = pbr.version.VersionInfo('ldappool') # The short X.Y version. version = version_info.version_string() # The full version, including alpha/beta/rc tags. release = version_info.release_string() # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['ldappool.'] # Grouping the document tree for man pages. # List of tuples 'sourcefile', 'target', 'title', 'Authors name', 'manual' #man_pages = [] # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. #html_theme_path = ["."] #html_theme = '_theme' html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%Y-%m-%d %H:%M' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'ldappooldoc' # -- Options for LaTeX output ------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]) # . latex_documents = [ ('index', 'ldappool.tex', 'ldappool Documentation', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True intersphinx_mapping = { 'python': ('http://docs.python.org/', None), } # -- Options for openstackdocstheme ------------------------------------------- repository_name = 'openstack/ldappool' bug_project = 'ldappool' bug_tag = '' ldappool-2.2.0/doc/source/history.rst0000666000175100017510000000003513234417673017663 0ustar zuulzuul00000000000000.. include:: ../../ChangeLog ldappool-2.2.0/doc/source/index.rst0000666000175100017510000000116513234417673017276 0ustar zuulzuul00000000000000ldappool ======== Contents: .. toctree:: :maxdepth: 1 api/modules Release Notes ============= .. toctree:: :maxdepth: 1 history Contributing ============ Code is hosted `on GitHub`_. Submit bugs to the Keystone project on `Launchpad`_. Submit code to the ``openstack/ldappool`` project using `Gerrit`_. .. _on GitHub: https://github.com/openstack/ldappool .. _Launchpad: https://launchpad.net/ldappool .. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow Run tests with ``tox``. Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ldappool-2.2.0/MANIFEST.in0000666000175100017510000000012013234417673015114 0ustar zuulzuul00000000000000include README.rst include MANIFEST.in include CHANGES.rst include CONTRIBUTORS ldappool-2.2.0/ldappool.egg-info/0000775000175100017510000000000013234420105016647 5ustar zuulzuul00000000000000ldappool-2.2.0/ldappool.egg-info/dependency_links.txt0000664000175100017510000000000113234420104022714 0ustar zuulzuul00000000000000 ldappool-2.2.0/ldappool.egg-info/PKG-INFO0000664000175100017510000000646013234420104017751 0ustar zuulzuul00000000000000Metadata-Version: 1.1 Name: ldappool Version: 2.2.0 Summary: A simple connector pool for python-ldap. Home-page: https://git.openstack.org/cgit/openstack/ldappool Author: OpenStack Author-email: UNKNOWN License: UNKNOWN Description-Content-Type: UNKNOWN Description: ldappool ======== A simple connector pool for pyldap. The pool keeps LDAP connectors alive and let you reuse them, drastically reducing the time spent to initiate a ldap connection. The pool has useful features like: - transparent reconnection on failures or server restarts - configurable pool size and connectors timeouts - configurable max lifetime for connectors - a context manager to simplify acquiring and releasing a connector **You need pyldap in order to use this library** Quickstart :::::::::: To work with the pool, you just need to create it, then use it as a context manager with the *connection* method:: from ldappool import ConnectionManager cm = ConnectionManager('ldap://localhost') with cm.connection('uid=adminuser,ou=logins,dc=mozilla', 'password') as conn: .. do something with conn .. The connector returned by *connection* is a LDAPObject, that's binded to the server. See https://github.com/pyldap/pyldap/ for details on how to use a connector. ConnectionManager options ::::::::::::::::::::::::: Here are the options you can use when instanciating the pool: - **uri**: ldap server uri **[mandatory]** - **bind**: default bind that will be used to bind a connector. **default: None** - **passwd**: default password that will be used to bind a connector. **default: None** - **size**: pool size. **default: 10** - **retry_max**: number of attempts when a server is down. **default: 3** - **retry_delay**: delay in seconds before a retry. **default: .1** - **use_tls**: activate TLS when connecting. **default: False** - **timeout**: connector timeout. **default: -1** - **use_pool**: activates the pool. If False, will recreate a connector each time. **default: True** The **connection** method takes two options: - **bind**: bind used to connect. If None, uses the pool default's. **default: None** - **passwd**: password used to connect. If None, uses the pool default's. **default: None** History ::::::: Prior to v2.0.0 ``ldappool`` required ``python-ldap``. As of v2.0.0 this library now required ``pyldap``, a python 3 compatible fork of ``python-ldap``. Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 ldappool-2.2.0/ldappool.egg-info/requires.txt0000664000175100017510000000001713234420104021244 0ustar zuulzuul00000000000000pyldap>=2.4.20 ldappool-2.2.0/ldappool.egg-info/pbr.json0000664000175100017510000000005613234420104020325 0ustar zuulzuul00000000000000{"git_version": "90aaa08", "is_release": true}ldappool-2.2.0/ldappool.egg-info/SOURCES.txt0000664000175100017510000000105213234420105020531 0ustar zuulzuul00000000000000.testr.conf AUTHORS CHANGES.rst CONTRIBUTORS ChangeLog MANIFEST.in README.rst requirements.txt setup.cfg setup.py test-requirements.txt tox.ini doc/Makefile doc/source/conf.py doc/source/history.rst doc/source/index.rst ldappool/__init__.py ldappool.egg-info/PKG-INFO ldappool.egg-info/SOURCES.txt ldappool.egg-info/dependency_links.txt ldappool.egg-info/not-zip-safe ldappool.egg-info/pbr.json ldappool.egg-info/requires.txt ldappool.egg-info/top_level.txt ldappool/tests/__init__.py ldappool/tests/test_ldapconnection.py ldappool/tests/test_ldappool.pyldappool-2.2.0/ldappool.egg-info/top_level.txt0000664000175100017510000000001113234420104021370 0ustar zuulzuul00000000000000ldappool ldappool-2.2.0/ldappool.egg-info/not-zip-safe0000664000175100017510000000000113234420073021101 0ustar zuulzuul00000000000000 ldappool-2.2.0/test-requirements.txt0000666000175100017510000000104113234417673017622 0ustar zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. # of appearance. hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 flake8-docstrings==0.2.1.post1 # MIT coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD sphinx!=1.6.6,>=1.6.2 # BSD openstackdocstheme>=1.17.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testresources>=2.0.0 # Apache-2.0/BSD testtools>=2.2.0 # MIT