From 6e06facef50739078260b7cc72991be7bffe475e Mon Sep 17 00:00:00 2001 From: Frank Brehm Date: Thu, 11 Mar 2021 16:11:38 +0100 Subject: [PATCH] Implementing migration of group entries --- lib/ldap_migration/__init__.py | 279 +++++++++++++++++++++++++++------ 1 file changed, 232 insertions(+), 47 deletions(-) diff --git a/lib/ldap_migration/__init__.py b/lib/ldap_migration/__init__.py index 5d898f3..8ff7bdc 100644 --- a/lib/ldap_migration/__init__.py +++ b/lib/ldap_migration/__init__.py @@ -47,7 +47,7 @@ from fb_tools.collections import FrozenCIStringSet, CIStringSet, CIDict from .config import LDAPMigrationConfiguration -__version__ = '0.10.8' +__version__ = '0.11.0' LOG = logging.getLogger(__name__) CFG_BASENAME = 'ldap-migration.ini' @@ -2025,7 +2025,7 @@ class LDAPMigrationApplication(BaseApplication): target_entry = {} used_classes = CIStringSet() - src_data = self.get_src_entry_data(src_entry, as_group=True) + src_data = self.normalize_entry_data(src_entry, as_group=True, is_src_entry=True) for src_oc_name in src_data['classes']: if src_oc_name.lower() not in ('groupofurls', 'groupofnames', 'groupofuniquenames'): tgt_oc_name = self.object_classes.real_key(src_oc_name) @@ -2140,83 +2140,268 @@ class LDAPMigrationApplication(BaseApplication): return members # ------------------------------------------------------------------------- - def get_src_entry_data(self, src_entry, as_group=False): + def normalize_entry_data(self, entry, as_group=False, is_src_entry=False): - src_classes = CIStringSet() - src_attributes = CIDict() - src_members = CIStringSet() - src_member_url = CIStringSet() + classes = CIStringSet() + attributes = CIDict() + members = CIStringSet() + member_url = CIStringSet() + dn = entry['dn'] + what = 'target entry' + if is_src_entry: + what = 'source entry' - for src_aname in src_entry['attributes']: + for aname in entry['attributes']: - src_attr = src_entry['attributes'][src_aname] + attribute = entry['attributes'][aname] - if src_aname.lower() == 'objectclass': - for src_oc_name in src_attr: - if src_oc_name not in self.object_classes: + if aname.lower() == 'objectclass': + for oc_name in attribute: + if oc_name not in self.object_classes: if self.verbose > 3: - msg = "ObjectClass {oc!r} of sorce entry {dn!r} not found " + msg = "ObjectClass {oc!r} of {what} {dn!r} not found " msg += "on target LDAP server." - msg = msg.format(oc=src_oc_name, dn=src_dn) + msg = msg.format(oc=oc_name, what=what, n=dn) LOG.debug(msg) - self.unknown_objectclasses.add(src_oc_name) + self.unknown_objectclasses.add(oc_name) continue - src_classes.add(src_oc_name) + norm_oc_name = self.object_classes.real_key(oc_name) + classes.add(norm_oc_name) + continue - elif src_aname.lower() in ('member', 'uniquemember') and as_group: + elif aname.lower() in ('member', 'uniquemember') and as_group: if self.verbose > 1: - LOG.debug("Transforming members: {!r}".format(src_attr)) - if is_sequence(src_attr): - for attr in src_attr: + LOG.debug("Transforming members: {!r}".format(attribute)) + if is_sequence(attribute): + for attr in attribute: member = self.mangle_dn(attr) - src_members.add(member) + members.add(member) else: - member = self.mangle_dn(src_attr) - src_members.add(member) - elif src_aname.lower() == 'memberurl' and as_group: - if is_sequence(src_attr): - for attr in src_attr: - src_member_url.add(attr) + member = self.mangle_dn(attribute) + members.add(member) + continue + + elif aname.lower() == 'memberurl' and as_group: + if is_sequence(attribute): + for attr in attribute: + member_url.add(attr) else: - src_member_url.add(src_attr) + member_url.add(attribute) + continue + else: - if is_sequence(src_attr): - for attr in src_attr: + if is_sequence(attribute): + for attr in attribute: if attr == '': continue - if src_aname not in src_attributes: - src_attributes[src_aname] = [] - if src_aname in self.boolean_attr_types: + if aname not in attributes: + attributes[aname] = [] + if aname in self.boolean_attr_types: if to_bool(attr): attr = 'TRUE' else: attr = 'FALSE' - src_attributes[src_aname].append(attr) - elif src_attr != '': - if src_aname not in src_attributes: - src_attributes[src_aname] = [] - if src_aname in self.boolean_attr_types: - if to_bool(src_attr): - src_attr = 'TRUE' + attributes[aname].append(attr) + elif attribute != '': + if aname not in attributes: + attributes[aname] = [] + if aname in self.boolean_attr_types: + if to_bool(attribute): + attribute = 'TRUE' else: - src_attr = 'FALSE' - src_attributes[src_aname].append(src_attr) + attribute = 'FALSE' + attributes[aname].append(attribute) ret = { - 'classes': src_classes, - 'attributes': src_attributes, + 'attributes': attributes, + 'classes': classes, + 'dn': dn, } if as_group: - ret['members'] = src_members - ret['member_url'] = src_member_url + ret['members'] = members + ret['member_url'] = member_url return ret # ------------------------------------------------------------------------- def migrate_existing_group_entry(self, src_dn, tgt_dn, src_entry, tgt_entry): + try: + changes = self.generate_modify_group_entry_data(src_entry, tgt_entry, src_dn, tgt_dn) + except UnicodeDecodeError as e: + msg = "Source attributes:\n{}".format(pp(src_entry['attributes'])) + LOG.debug(msg) + msg = "Current target attributes:\n{}".format(pp(tgt_entry['attributes'])) + LOG.debug(msg) + msg = "UnicodeDecodeError on generating changes for source DN {s!r}: {e}".format( + s=src_dn, e=e) + raise FatalLDAPMigrationError(msg) + if changes: + if self.verbose: + LOG.info("Updating target entry {!r} ...".format(tgt_dn)) + if self.verbose > 2: + msg = "Source attributes:\n{}".format(pp(src_entry['attributes'])) + LOG.debug(msg) + msg = "Current target attributes:\n{}".format(pp(tgt_entry['attributes'])) + LOG.debug(msg) + if self.verbose > 1: + msg = "Changes on target entry {tdn!r}:\n{ch}".format( + tdn=tgt_dn, ch=pp(changes)) + LOG.debug(msg) + self.count_modified += 1 + if not self.simulate: + try: + mod_status, mod_result, mod_response, _ = self.target.modify(tgt_dn, changes) + except LDAPException as e: + msg = "Modifying NOT successfull - {c}: {e}\n" + msg += "Source attributes:\n{sattr}\n" + msg += "Changes:\n{ch}" + msg = msg.format( + c=e.__class__.__name__, e=e, + sattr=pp(src_entry['attributes']), ch=pp(changes)) + raise WriteLDAPItemError(msg) + if mod_status: + LOG.debug("Modifying successfull.") + if self.verbose > 2: + LOG.debug("Result of modifying:\n{}".format(pp(mod_result))) + else: + msg = "Modifying NOT successfull:\n{res}\n" + msg += "Source attributes:\n{sattr}\n" + msg += "Changes:\n{ch}" + msg = msg.format( + res=pp(mod_result), sattr=pp(src_entry['attributes']), + ch=pp(changes)) + raise WriteLDAPItemError(msg) + self.migrated_entries[rev_dn] = tgt_dn + self.write_result_file(fh, tgt_dn, '+') + return True + else: + if self.verbose: + msg = "No changes on target entry {tdn!r} necessary.".format(tdn=tgt_dn) + LOG.info(msg) + self.write_result_file(fh, tgt_dn, ' ') + return False + return True + # ------------------------------------------------------------------------- + def generate_modify_group_entry_data(self, src_entry, tgt_entry, src_dn, tgt_dn): + + changes = {} + + src_obj_classes = CIStringSet() + tgt_obj_classes = CIStringSet() + members_to_have = CIStringSet() + members_having = CIStringSet() + + src_data = self.normalize_entry_data(src_entry, as_group=True, is_src_entry=True) + tgt_data = self.normalize_entry_data(tgt_entry, as_group=True, is_src_entry=False) + + for src_oc_name in src_data['classes']: + if src_oc_name.lower() not in ('groupofurls', 'groupofnames', 'groupofuniquenames'): + oc_name = self.object_classes.real_key(src_oc_name) + src_obj_classes.add(oc_name) + src_obj_classes.add(self.object_classes.real_key('groupOfUniqueNames')) + + for tgt_oc_name in tgt_data['classes']: + if tgt_oc_name.lower() not in ('groupofurls', 'groupofnames', 'groupofuniquenames'): + oc_name = self.object_classes.real_key(tgt_oc_name) + tgt_obj_classes.add(oc_name) + tgt_obj_classes.add(self.object_classes.real_key('groupOfUniqueNames')) + + oc_diff = src_obj_classes - tgt_obj_classes + if oc_diff: + if 'objectClass' not in changes: + changes['objectClass'] = [] + changes['objectClass'].append((MODIFY_ADD, oc_diff.as_list())) + + oc_diff = tgt_obj_classes - src_obj_classes + if oc_diff: + if 'objectClass' not in changes: + changes['objectClass'] = [] + changes['objectClass'].append((MODIFY_DELETE, oc_diff.as_list())) + + for member in src_data['members']: + members_to_have.add(member) + members_to_have += self.get_dyn_members(src_data['member_url']) + + for member in tgt_data['members']: + members_having.add(member) + members_having += self.get_dyn_members(tgt_data['member_url'], from_src=False) + + for at_name in src_data['attributes']: + + if ('obsolete' in self.attribute_types[at_name] and + self.attribute_types[at_name]['obsolete']): + if self.verbose > 1: + msg = "AttributeType {at!r} of sorce entry {dn!r} is obsolete.".format( + at=at_name, dn=src_dn) + LOG.debug(msg) + continue + + if at_name not in self.attribute_types: + if self.verbose > 3: + msg = "AttributeType {at!r} of sorce entry {dn!r} not found " + msg += "on target LDAP server." + msg = msg.format(at=at_name, dn=src_dn) + LOG.debug(msg) + continue + + src_value = src_data['attributes'][at_name] + + if at_name in tgt_data['attributes']: + cur_tgt_value = tgt_data['attributes'][at_name] + both_equal = False + if at_name in self.pure_binary_attr_types: + both_equal = self.compare_binary_values( + src_value, tgt_data['attributes'][at_name]) + else: + both_equal = compare_ldap_values(src_value, tgt_data['attributes'][at_name]) + if both_equal: + if self.verbose > 3: + msg = ( + "Attribute {atr!r} of source DN {sdn!r} is equal to " + "target DN {tdn!r}. Won't change.") + LOG.debug(msg.format(atr=at_name, sdn=src_dn, tdn=tgt_dn)) + continue + changes[at_name] = [(MODIFY_REPLACE, src_value)] + else: + changes[at_name] = [(MODIFY_ADD, src_value)] + + at_name = 'uniqueMember' + if members_to_have: + if compare_ldap_values(members_to_have, members_having): + if self.verbose > 3: + msg = ( + "Attribute {atr!r} of source DN {sdn!r} is equal to " + "target DN {tdn!r}. Won't change.") + LOG.debug(msg.format(atr=at_name, sdn=src_dn, tdn=tgt_dn)) + else: + if at_name not in changes: + changes[at_name] = [] + member_diff = members_to_have - members_having + if member_diff: + changes[at_name].append((MODIFY_ADD, member_diff.as_dict())) + member_diff = members_having - members_to_have + if member_diff: + changes[at_name].append((MODIFY_DELETE, member_diff.as_dict())) + + else: + if 'members' in tgt_data and tgt_data['members']: + if at_name not in changes: + changes[at_name] = [] + changes[at_name].append((MODIFY_DELETE)) + if 'member_url' in tgt_data and tgt_data['member_url']: + if 'memberUrl' not in changes: + changes['memberUrl'] = [] + changes['memberUrl'].append((MODIFY_DELETE)) + + if changes.keys(): + return changes + + self.count_unchanged += 1 + return None + # ------------------------------------------------------------------------- def detailled_summary(self): @@ -2288,7 +2473,7 @@ class LDAPMigrationApplication(BaseApplication): self.get_all_dns() self.get_structural_dns() self.migrate_entries() - # self.migrate_group_entries() + self.migrate_group_entries() self.detailled_summary() finally: -- 2.39.5