ldaptools/src/ldaptools/ldapsync/cmd.py

158 lines
6.0 KiB
Python

import argparse
import sys
import ldap.sasl
from ldaptools import ldap_source, ldif_utils, paged
from ldaptools.synchronize import Synchronize
def source_uri(value):
for prefix in ['ldapi://', 'ldap://', 'ldaps://']:
if value.startswith(prefix):
return value
raise argparse.ArgumentTypeError('%r is not an LDAP url' % value)
def or_type(f1, f2):
def f(value):
try:
return f1(value)
except argparse.ArgumentTypeError as e1:
try:
return f2(value)
except argparse.ArgumentTypeError as e2:
raise argparse.ArgumentTypeError('%s and %s' % (e1.args[0], e2.args[0]))
return f
def object_class_pivot(value):
value = list(filter(None, map(str.strip, map(str.lower, value.split()))))
if len(value) != 2:
raise argparse.ArgumentTypeError('%r is not a pair of an objectClass and an attribute name')
return value
def main(args=None):
parser = argparse.ArgumentParser(
description="""\
Synchronize an LDIF file or a source LDAP directory to another directory
Base DN of the source is remapped to another DN in the target directory"""
)
parser.add_argument(
'--object-class-pivot',
required=True,
type=object_class_pivot,
action='append',
help='an objectClass and an attribute name which is the unique identifier ' 'for this class',
)
parser.add_argument(
'--attributes-file',
type=argparse.FileType('r'),
help='a file containing the list of attributes to synchronize',
)
parser.add_argument('--attributes', help='a list of attribute names separated by spaces')
parser.add_argument(
'--source-uri',
required=True,
type=or_type(source_uri, argparse.FileType('r')),
help='URL of an LDAP directory (ldapi://, ldap:// or ldaps://) or path of ' 'and LDIF file',
)
parser.add_argument(
'--case-insensitive-attribute',
action='append',
help='indicate that the attribute must be compared case insensitively',
)
parser.add_argument('--source-base-dn', required=True, help='base DN of the source')
parser.add_argument('--source-bind-dn', help='bind DN for a source LDAP directory')
parser.add_argument('--source-bind-password', help='bind password for a source LDAP directory')
parser.add_argument('--source-filter', help='filter to apply to a source LDAP directory')
parser.add_argument('--source-objectclasses', help='keep only thoses object classes')
parser.add_argument(
'--target-uri', type=source_uri, required=True, help='URL of the target LDAP directory'
)
parser.add_argument('--target-base-dn', required=True, help='base DN of the target LDAP directory')
parser.add_argument('--target-bind-dn', help='bind DN for a target LDAP directory')
parser.add_argument('--target-bind-password', help='bind password for a target LDAP directory')
parser.add_argument(
'--fake', action='store_true', help='compute synchronization actions but do not apply'
)
parser.add_argument('--verbose', action='store_true', help='print all actions to stdout')
options = parser.parse_args(args=args)
attributes = set()
if options.attributes_file:
attributes.update([attribute.strip().lower() for attribute in options.attributes_file])
if options.attributes:
for attribute in options.attributes.split():
attribute = attribute.strip().lower()
if attribute:
attributes.add(attribute)
attributes = list(attributes)
if not attributes:
parser.print_help()
print('You must give at least one attribute to synchronize')
if options.verbose:
print('Synchronizing', end=' ')
if hasattr(options.source_uri, 'read'):
if options.verbose:
print(options.source_uri.name, end=' ')
source = ldif_utils.ListLDIFParser(options.source_uri)
source.parse()
else:
if options.verbose:
print(options.source_uri, end=' ')
conn = paged.PagedLDAPObject(options.source_uri)
if options.source_uri.startswith('ldapi://'):
conn.sasl_interactive_bind_s('', ldap.sasl.external())
elif options.source_bind_dn and options.source_bind_password:
conn.simple_bind_s(options.source_bind_dn, options.source_bind_password)
source = ldap_source.LDAPSource(
conn, base_dn=options.source_base_dn, attributes=attributes, filterstr=options.source_filter
)
if options.verbose:
print('to', options.target_uri, end=' ')
target_conn = paged.PagedLDAPObject(options.target_uri)
if options.target_uri.startswith('ldapi://'):
target_conn.sasl_interactive_bind_s('', ldap.sasl.external())
elif options.target_bind_dn and options.target_bind_dn:
target_conn.simple_bind_s(options.target_bind_dn, options.target_bind_password)
if options.source_objectclasses:
source_objectclasses = options.source_objectclasses.split()
else:
source_objectclasses = [v[0] for v in options.object_class_pivot]
synchronize = Synchronize(
source,
options.source_base_dn,
target_conn,
options.target_base_dn,
pivot_attributes=options.object_class_pivot,
objectclasses=source_objectclasses,
attributes=attributes,
case_insensitive_attribute=options.case_insensitive_attribute,
)
synchronize.build_actions()
if options.verbose:
for action in synchronize.actions:
print(' -', action)
if not synchronize.actions:
print('Nothing to do.')
if not options.fake:
synchronize.apply_actions()
failed_actions = [action for action in synchronize.actions if action.errors]
if failed_actions:
print('Some actions failed:', file=sys.stderr)
for action in failed_actions:
print(' -', action)
for error in action.errors:
print(' *', error)
raise SystemExit(1)