# 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
# 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
"""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):
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'])
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__":
# 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
from ..argparse_actions import NonNegativeItegerOptionAction
from ..argparse_actions import LimitedFloatOptionAction
-__version__ = '0.6.1'
+__version__ = '0.7.1'
LOG = logging.getLogger(__name__)
_ = XLATOR.gettext
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 = []
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.")
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__":