]> Frank Brehm's Git Trees - pixelpark/pp-admin-tools.git/commitdiff
Creating modify data for mirroring LDAP entries.
authorFrank Brehm <frank@brehm-online.com>
Mon, 31 Oct 2022 16:10:08 +0000 (17:10 +0100)
committerFrank Brehm <frank@brehm-online.com>
Mon, 31 Oct 2022 16:10:08 +0000 (17:10 +0100)
lib/pp_admintools/app/ldap.py
lib/pp_admintools/app/mirror_ldap.py

index 9396d0073532fa62f992ab092bd204cf6173b121..9ca0932236847a2e539ad15c60ad75651afe3997 100644 (file)
@@ -27,7 +27,7 @@ from ldap3 import BASE, SUBTREE
 # from ldap3 import BASE, LEVEL, SUBTREE, DEREF_NEVER, DEREF_SEARCH, DEREF_BASE, DEREF_ALWAYS
 from ldap3 import ALL_ATTRIBUTES
 # from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES
-from ldap3 import MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE
+from ldap3 import MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE
 # from ldap3.core.exceptions import LDAPInvalidDnError, LDAPInvalidValueError
 from ldap3.core.exceptions import LDAPException
 # from ldap3.core.exceptions import LDAPException, LDAPBindError
@@ -54,7 +54,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.10.1'
+__version__ = '0.10.2'
 LOG = logging.getLogger(__name__)
 
 _ = XLATOR.gettext
@@ -90,6 +90,11 @@ class DeleteLDAPItemError(FatalLDAPError):
     """Error class in case, a LDAP item could not be deleted."""
     pass
 
+# =============================================================================
+class LDAPParseError(FatalLDAPError):
+    """Error on parsing LDAP stuff."""
+    pass
+
 
 # =============================================================================
 class PasswordFileOptionAction(argparse.Action):
@@ -183,6 +188,9 @@ class BaseLdapApplication(BaseDPXApplication):
     pattern_dn_separator = r'\s*,\s*'
     re_dn_separator = re.compile(pattern_dn_separator)
 
+    pattern_dntoken = r'^([^=\s]+)\s*=\s*(\S.*)\S*$'
+    re_dntoken = re.compile(pattern_dntoken)
+
     person_object_classes = FrozenCIStringSet([
         'account', 'inetOrgPerson', 'inetUser', 'posixAccount', 'sambaSamAccount'])
 
@@ -1397,6 +1405,100 @@ class BaseLdapApplication(BaseDPXApplication):
                 return line.strip()
         return None
 
+    # -------------------------------------------------------------------------
+    def generate_modify_data(self, dn, src_attribs, tgt_attribs):
+
+        changes = {}
+
+        first_dn_token = self.re_dn_separator.split(dn)[0]
+        match = self.re_dntoken.match(first_dn_token)
+        if not match:
+            msg = _("Could not detect RDN from DN {!r}.").format(dn)
+            raise LDAPParseError(msg)
+        rdn = match.group(1)
+        if self.verbose > 2:
+            msg = _("Found RDN attribute {!r}.").format(rdn)
+            LOG.debug(msg)
+
+        for attrib_name in src_attribs:
+            if attrib_name.lower() == rdn.lower():
+                if self.verbose > 2:
+                    msg = _("RDN attribute {!r} will not be touched.").format(rdn)
+                    LOG.debug(msg)
+                continue
+
+            if attrib_name.lower() == 'memberof':
+                if self.verbose > 2:
+                    msg = _("Attribute {!r} will not be touched.").format(attrib_name)
+                    LOG.debug(msg)
+                continue
+
+            attr_changes = self._generate_diff_attribs(attrib_name, src_attribs, tgt_attribs)
+            if attr_changes:
+                changes[attrib_name] = attr_changes
+
+        for attrib_name in tgt_attribs:
+            if attrib_name in src_attribs:
+                continue
+
+            if attrib_name.lower() == rdn.lower():
+                msg = "RDN attribute {!r} will not be touched.".format(rdn)
+                LOG.warn(msg)
+                continue
+
+            if attrib_name.lower() == 'memberof':
+                if self.verbose > 2:
+                    msg = _("Attribute {!r} will not be touched.").format(attrib_name)
+                    LOG.debug(msg)
+                continue
+
+            if attrib_name not in changes:
+                changes[attrib_name] = []
+            changes[attrib_name].append((MODIFY_DELETE, ))
+
+        return changes
+
+    # -------------------------------------------------------------------------
+    def _generate_diff_attribs(self, attrib_name, src_attribs, tgt_attribs):
+
+        attr_changes = []
+
+        src_attrib_values = src_attribs[attrib_name]
+
+        if attrib_name in tgt_attribs:
+            tgt_attrib_values = tgt_attribs[attrib_name]
+            values_add = []
+            values_del = []
+
+            for src_val in src_attrib_values:
+                if src_val not in tgt_attrib_values:
+                    values_add.append(src_val)
+            for tgt_val in tgt_attrib_values:
+                if tgt_val not in src_attrib_values:
+                    values_del.append(tgt_val)
+
+            if self.verbose > 2 and values_add:
+                msg = _("Values to add to attribute {!r}:").format(attrib_name)
+                LOG.debug(msg + '\n' + pp(values_add))
+
+            if self.verbose > 2 and values_del:
+                msg = _("Values to removed from attribute {!r}:").format(attrib_name)
+                LOG.debug(msg + '\n' + pp(values_del))
+
+            if len(values_add) == len(src_attrib_values):
+                if len(values_add):
+                    attr_changes.append((MODIFY_REPLACE, values_add))
+            else:
+                if values_del:
+                    attr_changes.append((MODIFY_DELETE, values_del))
+                if values_add:
+                    attr_changes.append((MODIFY_ADD, values_add))
+
+        else:
+            attr_changes = [(MODIFY_ADD, src_attrib_values)]
+
+        return attr_changes
+
 
 # =============================================================================
 if __name__ == "__main__":
index be4cade22ee1502ace60a2374bdb26e25ef3e78f..fb2b58f161a5c5fb5ac6e481b3117a8dbc3caa7e 100644 (file)
@@ -18,6 +18,7 @@ from functools import cmp_to_key
 
 # Third party modules
 # from ldap3 import MODIFY_REPLACE, MODIFY_ADD, MODIFY_DELETE
+from ldap3 import ALL_ATTRIBUTES
 
 # Own modules
 # from fb_tools.common import to_bool, is_sequence
@@ -37,7 +38,7 @@ from .ldap import BaseLdapApplication
 from ..argparse_actions import NonNegativeItegerOptionAction
 from ..argparse_actions import LimitedFloatOptionAction
 
-__version__ = '0.6.1'
+__version__ = '0.7.1'
 LOG = logging.getLogger(__name__)
 
 _ = XLATOR.gettext
@@ -83,6 +84,7 @@ class MirrorLdapApplication(BaseLdapApplication):
         self.limit = 0
         self.wait_after_write = self.default_wait_after_write
         self.only_struct = False
+        self.mirrored_entries = 0
 
         self.structural_entr_dns = []
         self.non_structural_entr_dns = []
@@ -259,6 +261,7 @@ class MirrorLdapApplication(BaseLdapApplication):
             self.eval_sync_entries()
             self.clean_tgt_non_struct_entries()
             self.clean_tgt_struct_entries()
+            self.mirror_struct_entries()
 
         except KeyboardInterrupt:
             msg = _("Got a {}:").format('KeyboardInterrupt') + ' ' + _("Interrupted on demand.")
@@ -494,6 +497,70 @@ class MirrorLdapApplication(BaseLdapApplication):
             msg = _("None structural entries in target LDAP instance removed.")
         LOG.info(msg)
 
+    # -------------------------------------------------------------------------
+    def mirror_struct_entries(self):
+        """Mirroring all structurale entries."""
+        self.empty_line()
+        self.line(color='CYAN')
+        LOG.info(_("Mirroring structural entries from source to target LDAP instance."))
+        if not self.quiet:
+            time.sleep(2)
+
+        dns = sorted(self.src_struct_dns.as_list(), key=cmp_to_key(self.compare_ldap_dns))
+
+        count = 0
+
+        attributes = [ALL_ATTRIBUTES, 'aci']
+
+        for dn in dns:
+
+            self.empty_line()
+            LOG.info(_("Mirroring entry {!r} ...").format(dn))
+
+            src_entry = self.get_entry(dn, self.src_instance, attributes)
+            src_attribs = self.normalized_attributes(src_entry)
+            src_oclasses = src_attribs['objectClass'].as_list()
+            src_attribs_dict = src_attribs.dict()
+            src_attribs_dict['objectClass'] = src_oclasses
+
+            if self.verbose > 1:
+                LOG.debug("Got source entry:\n" + pp(src_attribs_dict))
+
+            tgt_entry = self.get_entry(dn, self.tgt_instance, attributes)
+            if tgt_entry:
+                tgt_attribs = self.normalized_attributes(tgt_entry)
+                tgt_oclasses = tgt_attribs['objectClass'].as_list()
+                tgt_attribs_dict = tgt_attribs.dict()
+                tgt_attribs_dict['objectClass'] = tgt_oclasses
+
+                if self.verbose > 1:
+                    LOG.debug("Got target entry:\n" + pp(tgt_attribs_dict))
+
+                changes = self.generate_modify_data(dn, src_attribs, tgt_attribs)
+                if changes:
+                    msg = _("Got modify data for DN {!r}:").format(dn)
+                    LOG.debug(msg + '\n' + pp(changes))
+                    self.mirrored_entries += 1
+                    count += 1
+                else:
+                    LOG.info(_("No changes necessary on DN {!r}.").format(dn))
+                    continue
+
+            else:
+                LOG.debug(_("Target entry {!r} not found.").format(dn))
+
+            if self.limit and self.mirrored_entries >= self.limit:
+                break
+
+        if count:
+            msg = ngettext(
+                "Mirrored one structural entry in target LDAP instance.",
+                "Mirrored {no} structural entries to target LDAP instance.",
+                count).format(no=count)
+        else:
+            msg = _("Mirrored no structural entries to target LDAP instance.")
+        LOG.info(msg)
+
 
 # =============================================================================
 if __name__ == "__main__":