]> Frank Brehm's Git Trees - pixelpark/pp-admin-tools.git/commitdiff
Refactoring LDAP search
authorFrank Brehm <frank.brehm@pixelpark.com>
Tue, 30 Jan 2024 17:05:27 +0000 (18:05 +0100)
committerFrank Brehm <frank.brehm@pixelpark.com>
Tue, 30 Jan 2024 17:05:27 +0000 (18:05 +0100)
lib/pp_admintools/app/ldap.py

index e7b5b3fe1960eac7a79c1201733e805f3ea39b66..9ddbb49db6b9742b002e31fca65eb6d40d144990 100644 (file)
@@ -35,11 +35,9 @@ from ldap3 import BASE, SUBTREE
 from ldap3 import Connection, DSA, IP_V4_PREFERRED, SAFE_SYNC, Server
 from ldap3 import MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE
 from ldap3.core.exceptions import LDAPBindError
-from ldap3.core.exceptions import LDAPCommunicationError
+from ldap3.core.exceptions import LDAPCommunicationError
 from ldap3.core.exceptions import LDAPException
 from ldap3.core.exceptions import LDAPSessionTerminatedByServerError
-from ldap3.core.exceptions import LDAPSocketOpenError
-from ldap3.core.exceptions import LDAPSocketReceiveError
 
 # Own modules
 from . import BaseDPXApplication
@@ -54,14 +52,15 @@ from ..config.ldap import DEFAULT_TIMEOUT
 from ..config.ldap import LdapConfiguration, LdapConnectionInfo
 # from ..errors import DpxAppError
 from ..errors import DpxDeleteLdapItemError
+from ..errors import DpxFatalLdapError
 from ..errors import DpxLdapError
 from ..errors import DpxLdapParseError
-from ..errors import DpxLdapReadError
+from ..errors import DpxLdapSearchError
 from ..errors import DpxLdapSessionError
 from ..errors import DpxWriteLdapItemError
 from ..xlate import XLATOR, format_list
 
-__version__ = '1.2.0'
+__version__ = '1.3.0'
 LOG = logging.getLogger(__name__)
 
 _ = XLATOR.gettext
@@ -885,52 +884,155 @@ class BaseLdapApplication(BaseDPXApplication):
             del self.ldap_server[inst]
 
     # -------------------------------------------------------------------------
-    def get_all_entries(self, inst, base_dn=None, ldap_filter=None, attributes=None, scope=None):
+    def search(
+            self, inst, search_base=None, ldap_filter=None, attributes=None, scope=None,
+            retries=None, wait_on_error=None, operational_attributes=False):
         """
-        Get all LDAP entries bellow the given BaseDN and the given LDAP filter.
+        Execute a LDAP search with the given parameters.
 
-        If no attributes are given, all attributes are given back.
-        The result is a hash with the DNs if the resulting entries as keys, and a hash
-        with the resulting attributes as values.
+        Returns a tuple with:
+            * status
+            * result
+            * response
         """
         connect_info = self.cfg.ldap_connection[inst]
 
         if scope is None:
             scope = SUBTREE
 
-        result = {}
+        rec_out = ''
+        if scope == SUBTREE:
+            rec_out = _('recursive') + ' '
 
-        if not base_dn:
-            base_dn = connect_info.base_dn
+        if not search_base:
+            search_base = connect_info.base_dn
         if attributes is None:
             attributes = [ALL_ATTRIBUTES]
         if ldap_filter is None:
             ldap_filter = '(objectClass=*)'
 
+        if retries is None:
+            retries = self.retries_on_conn_error
+
+        if wait_on_error is None:
+            wait_on_error = self.wait_on_conn_error
+
         if self.verbose > 2:
             msg = _(
-                'Searching in {uri}/{bdn} for all entries with filter {fltr!r}, '
-                'giving attributes:').format(uri=connect_info.url, bdn=base_dn, fltr=ldap_filter)
+                'Searching in {uri}/{bdn} {rec}for all entries with filter {fltr!r}, '
+                'giving attributes:').format(
+                    uri=connect_info.url, bdn=search_base, rec=rec_out, fltr=ldap_filter)
             msg += ' ' + format_list(attributes, do_repr=True)
             LOG.debug(msg)
 
+        cur_try = 0
+        search_status = None
+        search_result = None
+        search_response = None
+        result = None
+
+        while True:
+
+            cur_try += 1
+            result = self._search(
+                inst, cur_try=cur_try, retries=retries, search_base=search_base, scope=scope,
+                attributes=attributes, op_attrs=operational_attributes, ldap_filter=ldap_filter,
+                time_limit=self.cfg.ldap_timeout, wait_on_error=wait_on_error)
+
+            if result is not None:
+                break
+
+        search_status = result[0]
+        search_result = result[1]
+        search_response = result[2]
+
+        if search_status:
+            if self.verbose > 4:
+                LOG.debug(_('Result of searching:') + ' ' + pp(search_result))
+            if self.verbose > 3:
+                LOG.debug(_('Got a response entry:') + '\n' + pp(search_response[0]))
+
+        else:
+            if self.verbose > 3:
+                LOG.debug(_('Search was not successful.'))
+
+        return result
+
+    # -------------------------------------------------------------------------
+    def _search(
+            self, inst, cur_try, retries, search_base, scope, attributes,
+            op_attrs, ldap_filter, time_limit, wait_on_error):
+        """Execute underlaying LDAP search."""
+        e_cls = None
+        e_msg = None
+        search_status = None
+        search_result = None
+        search_response = None
+
+        if self.verbose > 2:
+            LOG.debug(_('Try number {} for searching ...').format(cur_try))
+
         if inst not in self.ldap_connection:
             self.connect_instance(inst)
         ldap = self.ldap_connection[inst]
 
         try:
-            req_status, req_result, req_response, req_whatever = ldap.search(
-                search_base=base_dn, search_scope=scope, attributes=attributes,
-                search_filter=ldap_filter, time_limit=self.cfg.ldap_timeout)
+            search_status, search_result, search_response, whatever = ldap.search(
+                search_base=search_base, search_scope=scope, attributes=attributes,
+                get_operational_attributes=op_attrs, search_filter=ldap_filter,
+                time_limit=time_limit)
+
+        except LDAPCommunicationError as e:
+            e_msg = str(e)
+            e_cls = e.__class__.__name__
+            if cur_try > retries:
+                msg = _('Got a {cls} on searching in LDAP instance {i!r}:').format(
+                    cls=e_cls, i=inst) + ' ' + str(e)
+                raise DpxLdapSearchError(msg)
+            LOG.debug(_('Waiting because of a {}:').format(e_cls) + ' ' + e_msg)
+            time.sleep(wait_on_error)
+            return None
+
+        except LDAPSessionTerminatedByServerError as e:
+            msg = _('Session terminated by server on searching on instance {i!r}:').format(
+                i=inst) + ' ' + str(e)
+            raise DpxLdapSessionError(msg)
+
+        except LDAPException as e:
+            msg = _('Got a {cls} on searching in LDAP instance {i!r}:').format(
+                cls=e.__class__.__name__, i=inst) + ' ' + str(e)
+            raise DpxFatalLdapError(msg)
+
         finally:
             if not self.single_session:
                 self.disconnect_instance(inst)
 
-        if req_status:
-            if self.verbose > 4:
-                LOG.debug(_('Result of searching:') + '\n' + pp(req_result))
+        return (search_status, search_result, search_response)
 
-            for entry in req_response:
+    # -------------------------------------------------------------------------
+    def get_all_entries(self, inst, base_dn=None, ldap_filter=None, attributes=None, scope=None):
+        """
+        Get all LDAP entries bellow the given BaseDN and the given LDAP filter.
+
+        If no attributes are given, all attributes are given back.
+        The result is a hash with the DNs if the resulting entries as keys, and a hash
+        with the resulting attributes as values.
+        """
+        connect_info = self.cfg.ldap_connection[inst]
+
+        result = {}
+        search_status = None
+        search_result = None
+        search_response = None
+
+        if not base_dn:
+            base_dn = connect_info.base_dn
+
+        (search_status, search_result, search_response) = self.search(
+            inst, search_base=base_dn, ldap_filter=ldap_filter, attributes=attributes)
+
+        if search_status:
+            for entry in search_response:
                 dn = entry['dn']
                 if self.verbose > 3:
                     LOG.debug(_('Found entry {!r}.').format(dn))
@@ -1343,66 +1445,22 @@ class BaseLdapApplication(BaseDPXApplication):
     def get_entry(self, dn, inst, attributes=None, operational_attributes=False, tries=3):
         """Get an complete LDAP entry by the given DN in the given instance."""
         connect_info = self.cfg.ldap_connection[inst]
-
-        if attributes is None:
-            attributes = [ALL_ATTRIBUTES]
-
-        sfilter = '(objectClass=*)'
-
-        result = None
-
         if self.verbose > 1:
             msg = _('Searching DN {dn!r} in {uri}.').format(dn=dn, uri=connect_info.url)
             LOG.debug(msg)
 
-        cur_try = 0
-        e_msg = None
-        while cur_try < tries:
-
-            if inst not in self.ldap_connection:
-                self.connect_instance(inst)
-            ldap = self.ldap_connection[inst]
-            e_msg = None
-            cur_try += 1
-            try:
-                req_status, req_result, req_response, req_whatever = ldap.search(
-                    search_base=dn, search_scope=BASE, attributes=attributes,
-                    get_operational_attributes=operational_attributes, search_filter=sfilter,
-                    time_limit=self.cfg.ldap_timeout)
-
-            except (LDAPSocketReceiveError, LDAPSocketOpenError) as e:
-                e_msg = str(e)
-                if cur_try >= tries:
-                    break
-                LOG.debug(_('Waiting because of a failing read operation.'))
-                time.sleep(self.wait_on_conn_error)
-
-            except LDAPSessionTerminatedByServerError as e:
-                msg = _('Session terminated on reading entry {dn!r} from instance {i!r}:').format(
-                    dn=dn, i=inst) + ' ' + str(e)
-                raise DpxLdapSessionError(msg)
-
-            finally:
-                if not self.single_session:
-                    self.disconnect_instance(inst)
-
-        if e_msg:
-            msg = _('Error on reading entry {dn!r} from instance {inst!r}:').format(
-                dn=dn, inst=inst) + ' ' + e_msg
-            raise DpxLdapReadError(msg)
+        result = None
 
-        if req_status:
-            if self.verbose > 4:
-                msg = _('Result of searching for DN {dn!r}:').format(dn=dn)
-                LOG.debug(msg + ' ' + pp(req_result))
-            result = req_response[0]
-            if self.verbose > 3:
-                LOG.debug(_('Got a response entry:') + '\n' + pp(result))
+        (search_status, search_result, search_response) = self.search(
+            inst=inst, search_base=dn, attributes=attributes, scope=BASE,
+            operational_attributes=operational_attributes)
 
+        if search_status:
+            result = search_response[0]
         else:
             if self.verbose > 3:
-                msg = _('Entry with DN {dn!r} not found in {uri}.').format(
-                    dn=dn, uri=connect_info.url)
+                msg = _('Entry with DN {dn!r} not found in {i!r} ({uri}).').format(
+                    dn=dn, i=inst, uri=connect_info.url)
                 LOG.debug(msg)
 
         return result