From 87b4839dbb69bade5b3b95c48d39a302c1871b21 Mon Sep 17 00:00:00 2001 From: Frank Brehm Date: Thu, 20 Oct 2022 17:03:54 +0200 Subject: [PATCH] Implemented cleaning of target LDAP instance for mirror-ldap --- lib/pp_admintools/app/ldap.py | 43 ++++++++++- lib/pp_admintools/app/mirror_ldap.py | 105 +++++++++++++++++++++++++-- 2 files changed, 141 insertions(+), 7 deletions(-) diff --git a/lib/pp_admintools/app/ldap.py b/lib/pp_admintools/app/ldap.py index e2a1ac6..3c707d9 100644 --- a/lib/pp_admintools/app/ldap.py +++ b/lib/pp_admintools/app/ldap.py @@ -53,7 +53,7 @@ from ..config.ldap import LdapConnectionInfo, LdapConfiguration # rom ..config.ldap import DEFAULT_PORT_LDAP, DEFAULT_PORT_LDAPS from ..config.ldap import DEFAULT_TIMEOUT -__version__ = '0.9.0' +__version__ = '0.10.0' LOG = logging.getLogger(__name__) _ = XLATOR.gettext @@ -792,10 +792,49 @@ class BaseLdapApplication(BaseDPXApplication): if result: result = sorted(result, key=cmp_to_key(self.compare_ldap_dns)) - if self.verbose > 2 and result: + if self.verbose > 3 and result: LOG.debug(_("Result:") + '\n' + pp(result)) return result + # ------------------------------------------------------------------------- + def get_all_entry_dns_hash(self, inst): + """Get Object classes and DNs of all entries in the given LDAP instance.""" + + connect_info = self.cfg.ldap_connection[inst] + base_dn = connect_info.base_dn + ldap = self.ldap_connection[inst] + + result = CIDict() + attributes = ['objectClass'] + ldap_filter = '(objectClass=*)' + + req_status, req_result, req_response, req_whatever = ldap.search( + search_base=base_dn, search_scope=SUBTREE, search_filter=ldap_filter, + get_operational_attributes=False, attributes=attributes, + time_limit=self.cfg.ldap_timeout) + + if req_status: + if self.verbose > 5: + msg = _("Result of searching for DNs of all entries:") + LOG.debug(msg + '\n' + pp(req_result)) + for entry in req_response: + if self.verbose > 4: + LOG.debug(_("Got a response entry:") + ' ' + pp(entry)) + + dn = entry['dn'] + object_classes = FrozenCIStringSet(entry['attributes']['objectClass']) + result[dn] = { + 'childs': CIStringSet(), + 'dn': dn, + 'object_classes': object_classes, + 'path': list(reversed(self.re_dn_separator.split(dn))), + } + + else: + LOG.warn("Got no entry DNs.") + + return result + # ------------------------------------------------------------------------- def get_user_dn(self, user, inst): diff --git a/lib/pp_admintools/app/mirror_ldap.py b/lib/pp_admintools/app/mirror_ldap.py index 9f04ef9..1b3c859 100644 --- a/lib/pp_admintools/app/mirror_ldap.py +++ b/lib/pp_admintools/app/mirror_ldap.py @@ -11,13 +11,19 @@ from __future__ import absolute_import # Standard modules import sys import logging +import copy +import time + +from functools import cmp_to_key # Third party modules # from ldap3 import MODIFY_REPLACE, MODIFY_ADD, MODIFY_DELETE # Own modules # from fb_tools.common import to_bool, is_sequence, pp -# from fb_tools.common import pp +from fb_tools.common import pp +# from fb_tools.collections import FrozenCIStringSet, CIStringSet, CIDict +from fb_tools.collections import CIDict, CIStringSet from ..xlate import XLATOR @@ -28,7 +34,7 @@ from .ldap import BaseLdapApplication from ..argparse_actions import NonNegativeItegerOptionAction from ..argparse_actions import LimitedFloatOptionAction -__version__ = '0.3.0' +__version__ = '0.4.0' LOG = logging.getLogger(__name__) _ = XLATOR.gettext @@ -66,7 +72,8 @@ class MirrorLdapApplication(BaseLdapApplication): self.tgt_connect_info = None self.src_dns = [] - self.tgt_dns_current = [] + self.tgt_dns_current = CIDict() + self.registered_tgt_dns = CIDict() self.limit = 0 self.wait_after_write = self.default_wait_after_write @@ -212,7 +219,13 @@ class MirrorLdapApplication(BaseLdapApplication): self.empty_line() LOG.info("I'm walking, yes indeed I'm walking ...") - self.clean_target_instance() + try: + self.clean_target_instance() + + except KeyboardInterrupt: + msg = _("Got a {}:").format('KeyboardInterrupt') + ' ' + _("Interrupted on demand.") + LOG.error(msg) + self.exit(10) # ------------------------------------------------------------------------- def clean_target_instance(self): @@ -224,6 +237,8 @@ class MirrorLdapApplication(BaseLdapApplication): "(except the base DN entry, of course).")) self.get_current_tgt_entries() + self.clean_tgt_non_struct_entries() + self.clean_tgt_struct_entries() # ------------------------------------------------------------------------- def get_current_tgt_entries(self): @@ -231,7 +246,87 @@ class MirrorLdapApplication(BaseLdapApplication): LOG.debug(_("Trying to get DNs of all entries in the target LDAP instance.")) - result = self.get_all_entry_dns(self.tgt_instance) + self.tgt_dns_current = self.get_all_entry_dns_hash(self.tgt_instance) + + for dn in sorted(list(self.tgt_dns_current.keys()), key=cmp_to_key(self.compare_ldap_dns)): + self.register_dn_tokens(dn, self.tgt_dns_current[dn], self.tgt_dns_current) + + if self.verbose > 4: + LOG.debug("Current target entries:\n" + pp(self.tgt_dns_current.dict())) + + # ------------------------------------------------------------------------- + def register_dn_tokens(self, dn, entry, registry): + + if self.verbose > 4: + LOG.debug("Trying to register DN {!r} ...".format(dn)) + + parent_tokens = copy.copy(entry['path'])[0:-1] + if not parent_tokens: + registry[dn]['parent'] = None + return + parent_dn = ','.join(reversed(parent_tokens)) + if self.verbose > 4: + LOG.debug("Parent DN: {!r}.".format(parent_dn)) + registry[dn]['parent'] = parent_dn + if parent_dn not in registry: + if self.verbose > 1: + LOG.debug("Entry {!r} seems to be a the root DN.".format(dn)) + return + + if not 'childs' not in registry[parent_dn]: + registry[parent_dn]['childs'] = CIStringSet() + registry[parent_dn]['childs'].add(dn) + + # ------------------------------------------------------------------------- + def clean_tgt_non_struct_entries(self): + """Removing all non structural entries in target instance. + + Structural entries are entries without any childs. + """ + + self.empty_line() + LOG.info(_("Removing all non structural entries from target LDAP instance.")) + if not self.quiet: + time.sleep(2) + self.empty_line() + + for dn in sorted(list(self.tgt_dns_current.keys()), key=cmp_to_key(self.compare_ldap_dns)): + entry = self.tgt_dns_current[dn] + if 'childs' not in entry: + LOG.error("Found entry {dn!r}:\n{e}".format(dn=dn, e=pp(entry))) + self.exit(5) + if entry['childs']: + if self.verbose > 1: + LOG.debug(_( + "Entry {!r} is a structural entry, will not be removed " + "at this point.").format(dn)) + continue + self.delete_entry(self.tgt_instance, dn) + if self.wait_after_write and not self.simulate: + time.sleep(self.wait_after_write) + + # ------------------------------------------------------------------------- + def clean_tgt_struct_entries(self): + """Removing all structural entries in target instance. + + Structural entries are entries without any childs. + """ + + self.empty_line() + LOG.info(_("Removing all structural entries from target LDAP instance.")) + if not self.quiet: + time.sleep(2) + self.empty_line() + + dns = sorted(list(self.tgt_dns_current.keys()), key=cmp_to_key(self.compare_ldap_dns)) + + for dn in list(reversed(dns[1:])): + entry = self.tgt_dns_current[dn] + if not entry['childs']: + continue + self.delete_entry(self.tgt_instance, dn) + if self.wait_after_write and not self.simulate: + time.sleep(self.wait_after_write) # ============================================================================= -- 2.39.5