From: Frank Brehm Date: Mon, 31 Oct 2022 16:10:08 +0000 (+0100) Subject: Creating modify data for mirroring LDAP entries. X-Git-Tag: 0.7.0^2~1^2~16 X-Git-Url: https://git.uhu-banane.org/?a=commitdiff_plain;h=4b1e98058fe2502f5906880a256b76cacc16cfc4;p=pixelpark%2Fpp-admin-tools.git Creating modify data for mirroring LDAP entries. --- diff --git a/lib/pp_admintools/app/ldap.py b/lib/pp_admintools/app/ldap.py index 9396d00..9ca0932 100644 --- a/lib/pp_admintools/app/ldap.py +++ b/lib/pp_admintools/app/ldap.py @@ -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__": diff --git a/lib/pp_admintools/app/mirror_ldap.py b/lib/pp_admintools/app/mirror_ldap.py index be4cade..fb2b58f 100644 --- a/lib/pp_admintools/app/mirror_ldap.py +++ b/lib/pp_admintools/app/mirror_ldap.py @@ -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__":