[1218] | 1 | #!/usr/bin/python
|
---|
| 2 | # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
|
---|
| 3 |
|
---|
[1260] | 4 | from __future__ import print_function
|
---|
| 5 |
|
---|
[1221] | 6 | # core modules
|
---|
[1219] | 7 | import argparse
|
---|
[1260] | 8 | try:
|
---|
| 9 | from configparser import ConfigParser
|
---|
| 10 | except ImportError:
|
---|
| 11 | from ConfigParser import ConfigParser
|
---|
[1221] | 12 | import logging
|
---|
| 13 | from pprint import pprint
|
---|
| 14 | import signal
|
---|
| 15 | import subprocess
|
---|
| 16 | import sys
|
---|
| 17 | import time
|
---|
| 18 |
|
---|
| 19 | # external modules
|
---|
[1219] | 20 | import datetime
|
---|
| 21 | import dateutil.parser
|
---|
| 22 | import dateutil.tz
|
---|
[1218] | 23 | import ldap
|
---|
[1221] | 24 | from ldap.ldapobject import ReconnectLDAPObject
|
---|
[1219] | 25 | import ldap.modlist
|
---|
[1221] | 26 | from ldap.syncrepl import SyncreplConsumer
|
---|
[1259] | 27 | from ldapurl import LDAPUrl
|
---|
[1218] | 28 | import ldif
|
---|
| 29 |
|
---|
[1221] | 30 |
|
---|
| 31 |
|
---|
[1219] | 32 | def getArguments():
|
---|
| 33 | configfile = '/etc/dassldapsync.conf'
|
---|
[1221] | 34 | parser = argparse.ArgumentParser(description='Synchronize the content of two LDAP servers.')
|
---|
[1219] | 35 | parser.add_argument('-d', '--debug', action='store_true', help="enable debug output")
|
---|
[1221] | 36 | parser.add_argument('configfile', default=configfile,
|
---|
| 37 | help="Configuration file [default: {}]".format(configfile))
|
---|
| 38 | return parser.parse_args()
|
---|
[1218] | 39 |
|
---|
[1219] | 40 |
|
---|
[1221] | 41 | class Options(object):
|
---|
[1218] | 42 | def __init__(self):
|
---|
[1221] | 43 | self.delete = True
|
---|
| 44 | self.starttls = False
|
---|
| 45 | self.updateonly = False
|
---|
| 46 | self.filter = None
|
---|
| 47 | self.attrlist = None
|
---|
| 48 | self.exclude = None
|
---|
| 49 | self.renameattr = None
|
---|
| 50 | self.renamecommand = None
|
---|
| 51 | self.pwd_max_days = 0
|
---|
| 52 |
|
---|
[1218] | 53 | def readLDIFSource(path):
|
---|
[1219] | 54 | logger = logging.getLogger()
|
---|
| 55 | logger.info("reading LDAP objects from file {}".format(path))
|
---|
[1221] | 56 | with open(path, 'r') as f:
|
---|
[1218] | 57 | parser = ldif.LDIFRecordList(f)
|
---|
| 58 | parser.parse()
|
---|
| 59 | result = parser.all_records
|
---|
| 60 | return result
|
---|
| 61 |
|
---|
[1221] | 62 | def readLdapSource(server, binddn, bindpw, basedn, filterstr, attrlist=None, starttls=False):
|
---|
[1219] | 63 | logger = logging.getLogger()
|
---|
| 64 | logger.info("reading LDAP objects from server {}".format(server))
|
---|
[1259] | 65 | ldapurl = LDAPUrl(hostport="{}:389".format(self.server))
|
---|
| 66 | con = ldap.initialize(ldapurl. initializeUrl())
|
---|
[1218] | 67 | if starttls:
|
---|
[1221] | 68 | con.start_tls_s()
|
---|
| 69 | con.simple_bind_s(binddn, bindpw)
|
---|
| 70 | results = con.search_s(basedn, ldap.SCOPE_SUBTREE, filterstr, attrlist)
|
---|
[1218] | 71 | return results
|
---|
| 72 |
|
---|
[1220] | 73 | class LdapSync(object):
|
---|
[1221] | 74 | def __init__(self, destserver,
|
---|
| 75 | destbinddn, destbindpw,
|
---|
| 76 | srcbasedn, destbasedn, options=Options()):
|
---|
[1219] | 77 | self.logger = logging.getLogger()
|
---|
[1221] | 78 |
|
---|
[1219] | 79 | self.destserver = destserver
|
---|
| 80 | self.destbasedn = destbasedn
|
---|
| 81 | self.destbinddn = destbinddn
|
---|
| 82 | self.destbindpw = destbindpw
|
---|
| 83 | self.options = options
|
---|
[1221] | 84 |
|
---|
[1219] | 85 | self.srcbasedn = srcbasedn
|
---|
[1218] | 86 |
|
---|
[1219] | 87 | self.con = None
|
---|
[1218] | 88 |
|
---|
[1221] | 89 | self.attrmap = ldap.cidict.cidict({})
|
---|
| 90 | self.classmap = {}
|
---|
[1218] | 91 |
|
---|
[1259] | 92 | #self.junk_objectclasses = [ b"sambaidmapentry" ]
|
---|
| 93 | #"sambasid",
|
---|
| 94 | self.junk_objectclasses = []
|
---|
| 95 | self.junk_attrs = ["authzto",
|
---|
| 96 | "creatorsname", "createtimestamp", "contextcsn",
|
---|
| 97 | "entrycsn", "entryuuid",
|
---|
| 98 | "memberof", "modifiersname", "modifytimestamp",
|
---|
| 99 | "pwdaccountlockedtime", "pwdchangedtime", "pwdfailuretime",
|
---|
| 100 | "structuralobjectclass"]
|
---|
[1221] | 101 |
|
---|
| 102 | self.reset_result()
|
---|
| 103 |
|
---|
| 104 |
|
---|
| 105 | def reset_result(self):
|
---|
| 106 | self.result = {
|
---|
| 107 | 'add': {'ok': [], 'failed': []},
|
---|
| 108 | 'update': {'ok': [], 'failed': []},
|
---|
| 109 | 'delete': {'ok': [], 'failed': []},
|
---|
| 110 | }
|
---|
| 111 |
|
---|
| 112 |
|
---|
| 113 | def _dest_ldap_connect(self):
|
---|
[1219] | 114 | if self.con is None:
|
---|
| 115 | self.logger.info("connect to destination LDAP server {}".format(self.destserver))
|
---|
[1259] | 116 | ldapurl = LDAPUrl(hostport="{}:389".format(self.destserver))
|
---|
| 117 | self.con = ldap.initialize(ldapurl. initializeUrl())
|
---|
[1219] | 118 | if self.options.starttls:
|
---|
| 119 | self.con.start_tls_s()
|
---|
[1221] | 120 | self.con.simple_bind_s(self.destbinddn, self.destbindpw)
|
---|
[1218] | 121 |
|
---|
[1219] | 122 | def __adapt_dn(self, dn):
|
---|
| 123 | # move LDAP object to dest base
|
---|
| 124 | if self.srcbasedn != self.destbasedn:
|
---|
| 125 | dn_old = dn
|
---|
[1221] | 126 | rpath = dn[:-len(self.srcbasedn)]
|
---|
| 127 | dn = rpath+self.destbasedn
|
---|
[1219] | 128 | self.logger.debug("moved {} to {}".format(dn_old, dn))
|
---|
| 129 | # print "dn:",dn,"src:",srcbasedn,"rpath:",rpath,"dest:",destbasedn
|
---|
| 130 | return dn
|
---|
[1218] | 131 |
|
---|
[1219] | 132 | def __is_dn_included(self, dn):
|
---|
| 133 | if self.options.exclude is None:
|
---|
| 134 | return True
|
---|
| 135 | if dn.lower().endswith(self.options.exclude):
|
---|
| 136 | return False
|
---|
| 137 | return True
|
---|
[1218] | 138 |
|
---|
[1219] | 139 | def __adapt_source_ldap_objects(self, searchresult):
|
---|
| 140 | """
|
---|
| 141 | Do configured modification to the source LDAP objects.
|
---|
| 142 | """
|
---|
| 143 | self.logger.debug("modifying LDAP objects retrieved from source LDAP")
|
---|
[1218] | 144 |
|
---|
[1221] | 145 | update_objects = []
|
---|
| 146 |
|
---|
[1219] | 147 | for r in searchresult:
|
---|
| 148 | dn = self.__adapt_dn(r[0])
|
---|
[1221] | 149 | d = ldap.cidict.cidict(r[1])
|
---|
[1218] | 150 |
|
---|
[1219] | 151 | if self.__is_dn_included(dn):
|
---|
[1221] | 152 | objectclasses = d["objectclass"]
|
---|
[1218] | 153 |
|
---|
[1221] | 154 | newObjectclasses = []
|
---|
[1219] | 155 | for o in objectclasses:
|
---|
| 156 | if o.lower() in self.classmap:
|
---|
[1221] | 157 | new_oc = self.classmap[o.lower()]
|
---|
| 158 | if new_oc not in newObjectclasses:
|
---|
| 159 | newObjectclasses.append(new_oc)
|
---|
[1219] | 160 | else:
|
---|
[1221] | 161 | if o not in newObjectclasses:
|
---|
[1219] | 162 | newObjectclasses.append(o)
|
---|
| 163 |
|
---|
[1221] | 164 | d["objectclass"] = newObjectclasses
|
---|
[1219] | 165 |
|
---|
| 166 | for a in d.keys():
|
---|
[1221] | 167 | attr = a
|
---|
[1219] | 168 | if self.attrmap.has_key(a.lower()):
|
---|
[1221] | 169 | attr = self.attrmap[attr].lower()
|
---|
| 170 | if attr.lower() != a.lower():
|
---|
| 171 | values = d[a]
|
---|
[1219] | 172 | del d[a]
|
---|
[1221] | 173 | d[attr] = values
|
---|
[1219] | 174 |
|
---|
[1221] | 175 | update_objects.append((dn, d))
|
---|
[1219] | 176 | return update_objects
|
---|
| 177 |
|
---|
| 178 |
|
---|
[1221] | 179 | def _get_dest_entry(self, dn, entry):
|
---|
[1219] | 180 | """
|
---|
| 181 | In the destination LDAP, the objects should be named
|
---|
| 182 | according to options.renameattr.
|
---|
| 183 | """
|
---|
[1221] | 184 | attrlist = self.options.attrlist
|
---|
| 185 |
|
---|
[1219] | 186 | existingDestDn = None
|
---|
| 187 | existingDestEntry = None
|
---|
| 188 | if self.options.renameattr and entry.has_key(self.options.renameattr):
|
---|
[1221] | 189 | searchresult = self.con.search_s(
|
---|
| 190 | self.destbasedn,
|
---|
| 191 | ldap.SCOPE_SUBTREE,
|
---|
| 192 | '%s=%s' % (self.options.renameattr, entry[self.options.renameattr][0]), attrlist)
|
---|
| 193 | if searchresult is not None and len(searchresult) > 0:
|
---|
[1219] | 194 | existingDestDn, existingDestEntry = searchresult[0]
|
---|
| 195 | if existingDestDn.lower() != dn.lower():
|
---|
[1221] | 196 | self.con.modrdn_s(existingDestDn, dn)
|
---|
| 197 | self.notify_renamed(existingDestDn, dn,
|
---|
| 198 | existingDestEntry[self.options.renameattr][0],
|
---|
| 199 | entry[self.options.renameattr][0],
|
---|
| 200 | options)
|
---|
[1219] | 201 | if existingDestDn is None:
|
---|
[1221] | 202 | searchresult = self.con.search_s(dn, ldap.SCOPE_BASE, 'objectclass=*', attrlist)
|
---|
[1219] | 203 | existingDestDn, existingDestEntry = searchresult[0]
|
---|
| 204 | return (existingDestDn, existingDestEntry)
|
---|
| 205 |
|
---|
| 206 |
|
---|
| 207 | def __handle_pwdAccountLockedTime(self, dn, entry, now, max_age):
|
---|
| 208 | # hack for syncing accounts locked by password policy
|
---|
| 209 | do_unlock = False
|
---|
[1221] | 210 | if self.options.pwd_max_days > 0 and entry.has_key('pwdChangedTime'):
|
---|
[1219] | 211 | # print "pwdChangedTime set for",dn
|
---|
| 212 | pwdChange = entry['pwdChangedTime'][0]
|
---|
| 213 | d = dateutil.parser.parse(pwdChange)
|
---|
[1221] | 214 | if (now-d) > max_age:
|
---|
| 215 | entry['pwdAccountLockedTime'] = ['000001010000Z']
|
---|
| 216 | self.logger.info("locking {} {}".format(dn, pwdChange))
|
---|
[1218] | 217 | else:
|
---|
[1219] | 218 | # pwdAccountLockedTime is a operational attribute,
|
---|
| 219 | # and therefore not part of entry.
|
---|
| 220 | # Do extra search to retrieve attribute.
|
---|
[1221] | 221 | searchresult = self.con.search_s(
|
---|
| 222 | dn, ldap.SCOPE_BASE,
|
---|
| 223 | "objectclass=*", attrlist=['pwdAccountLockedTime'])
|
---|
[1219] | 224 | tmp_dn, tmp_entry = searchresult[0]
|
---|
| 225 | if tmp_entry.has_key('pwdAccountLockedTime'):
|
---|
| 226 | do_unlock = True
|
---|
| 227 | return do_unlock
|
---|
[1218] | 228 |
|
---|
| 229 |
|
---|
[1221] | 230 | def _syncLdapObject(self, srcDn, srcAttributes):
|
---|
[1219] | 231 | tzutc = dateutil.tz.gettz('UTC')
|
---|
| 232 | now = datetime.datetime.now(tzutc)
|
---|
| 233 | max_age = datetime.timedelta(days=self.options.pwd_max_days)
|
---|
[1218] | 234 |
|
---|
[1259] | 235 | objectClasses = srcAttributes['objectClass']
|
---|
| 236 | srcAttributes['objectClass'] = [oc for oc in objectClasses if oc.lower() not in self.junk_objectclasses]
|
---|
| 237 |
|
---|
[1221] | 238 | try:
|
---|
| 239 | destDn, destAttributes = self._get_dest_entry(srcDn, srcAttributes)
|
---|
[1218] | 240 |
|
---|
[1221] | 241 | # hack for syncing accounts locked by password policy
|
---|
| 242 | do_unlock = self.__handle_pwdAccountLockedTime(srcDn, srcAttributes, now, max_age)
|
---|
[1218] | 243 |
|
---|
[1221] | 244 | mod_attrs = ldap.modlist.modifyModlist(destAttributes, srcAttributes)
|
---|
[1218] | 245 |
|
---|
[1221] | 246 | # hack for unlocking, see above
|
---|
| 247 | if do_unlock:
|
---|
| 248 | self.logger.info("unlocking {} {}".format(destDn, 'pwdAccountLockedTime'))
|
---|
| 249 | mod_attrs.append((ldap.MOD_DELETE, 'pwdAccountLockedTime', None))
|
---|
[1218] | 250 |
|
---|
[1221] | 251 | if self.options.attrlist is not None:
|
---|
| 252 | mod_attrs = [a for a in mod_attrs if a[1].lower() in self.options.attrlist]
|
---|
[1218] | 253 |
|
---|
[1221] | 254 | if self.junk_attrs is not None:
|
---|
| 255 | mod_attrs = [a for a in mod_attrs if a[1].lower() not in self.junk_attrs]
|
---|
[1218] | 256 |
|
---|
[1221] | 257 | if mod_attrs:
|
---|
| 258 | try:
|
---|
| 259 | self.logger.debug('mod_attrs: ' + str(mod_attrs))
|
---|
| 260 | self.con.modify_s(srcDn, mod_attrs)
|
---|
| 261 | self.notify_modified(srcDn)
|
---|
| 262 | except:
|
---|
| 263 | self.logger.exception('modify failed')
|
---|
| 264 | self.notify_modified(srcDn, False)
|
---|
[1259] | 265 | else:
|
---|
| 266 | self.notify_unchanged(srcDn)
|
---|
[1218] | 267 |
|
---|
[1221] | 268 | except ldap.NO_SUCH_OBJECT:
|
---|
| 269 | if not self.options.updateonly:
|
---|
[1219] | 270 | try:
|
---|
[1259] | 271 | entry = ldap.modlist.addModlist(srcAttributes, self.junk_attrs)
|
---|
| 272 | self.con.add_s(srcDn, entry)
|
---|
[1221] | 273 | self.notify_created(srcDn)
|
---|
| 274 | except (ldap.OBJECT_CLASS_VIOLATION,
|
---|
| 275 | ldap.NO_SUCH_OBJECT,
|
---|
[1259] | 276 | ldap.CONSTRAINT_VIOLATION) as e:
|
---|
| 277 | #print(e)
|
---|
[1221] | 278 | self.notify_created(srcDn, False)
|
---|
[1218] | 279 |
|
---|
[1221] | 280 |
|
---|
| 281 | def __syncLdapDestination(self, update_objects):
|
---|
| 282 |
|
---|
| 283 | logger.debug("writing data to destination LDAP")
|
---|
| 284 | for obj in update_objects:
|
---|
| 285 | dn, entry = obj
|
---|
| 286 | self._syncLdapObject(dn, entry)
|
---|
| 287 |
|
---|
| 288 |
|
---|
[1219] | 289 | def __deleteDestLdapObjects(self, update_objects):
|
---|
| 290 | """
|
---|
| 291 | Remove all LDAP objects in destination LDAP server
|
---|
| 292 | that did not come from the source LDAP objects
|
---|
| 293 | and are not excluded.
|
---|
| 294 | """
|
---|
[1218] | 295 |
|
---|
[1221] | 296 | searchresult = self.con.search_s(self.destbasedn, ldap.SCOPE_SUBTREE, self.options.filter)
|
---|
| 297 | existing = [x[0].lower() for x in searchresult]
|
---|
[1218] | 298 |
|
---|
[1221] | 299 | morituri = existing
|
---|
[1218] | 300 |
|
---|
[1219] | 301 | if self.destbasedn.lower() in existing:
|
---|
| 302 | morituri.remove(self.destbasedn.lower())
|
---|
[1218] | 303 |
|
---|
[1221] | 304 | for obj in update_objects:
|
---|
| 305 | dn, entry = obj
|
---|
[1218] | 306 | if dn.lower() in existing:
|
---|
| 307 | morituri.remove(dn.lower())
|
---|
| 308 | for dn in morituri:
|
---|
[1219] | 309 | if self.__is_dn_included(dn):
|
---|
| 310 | try:
|
---|
| 311 | self.con.delete_s(dn)
|
---|
[1221] | 312 | self.notify_deleted(dn)
|
---|
[1219] | 313 | except:
|
---|
[1221] | 314 | self.notify_deleted(dn, False)
|
---|
[1218] | 315 |
|
---|
[1221] | 316 |
|
---|
[1219] | 317 | def sync(self, searchresult):
|
---|
| 318 | """
|
---|
| 319 | Synchronize entries from searchresult to destination LDAP server.
|
---|
| 320 | """
|
---|
[1221] | 321 | if len(searchresult) == 0:
|
---|
[1219] | 322 | self.logger.error("empty source, aborting")
|
---|
| 323 | return
|
---|
| 324 |
|
---|
[1221] | 325 | self._dest_ldap_connect()
|
---|
[1219] | 326 |
|
---|
| 327 | update_objects = self.__adapt_source_ldap_objects(searchresult)
|
---|
[1221] | 328 | self.__syncLdapDestination(update_objects)
|
---|
| 329 | if self.options.delete and not self.options.updateonly:
|
---|
| 330 | self.__deleteDestLdapObjects(update_objects)
|
---|
[1219] | 331 | self.con.unbind()
|
---|
[1218] | 332 |
|
---|
[1221] | 333 | self.__log_summary(True)
|
---|
[1218] | 334 |
|
---|
[1219] | 335 |
|
---|
[1221] | 336 | def __log_summary(self, show_failed=True, show_ok=False):
|
---|
| 337 | result = self.result
|
---|
[1219] | 338 | for action in result.keys():
|
---|
| 339 | ok = len(result[action]['ok'])
|
---|
| 340 | failed = len(result[action]['failed'])
|
---|
[1259] | 341 | print("{} (ok: {}, failed: {}):".format(action, ok, failed))
|
---|
[1219] | 342 |
|
---|
| 343 | if show_ok and ok > 0:
|
---|
[1259] | 344 | print("succeeded:")
|
---|
| 345 | print("\n".join(result[action]['ok']))
|
---|
[1219] | 346 |
|
---|
| 347 | if show_failed and failed > 0:
|
---|
[1259] | 348 | print("failed:")
|
---|
| 349 | print("\n".join(result[action]['failed']))
|
---|
[1219] | 350 |
|
---|
[1221] | 351 | def get_short_dn(self, dn):
|
---|
| 352 | return dn.lower().replace(',' + self.srcbasedn.lower(), '')
|
---|
[1219] | 353 |
|
---|
[1259] | 354 | def notify_unchanged(self, dn):
|
---|
| 355 | logger.debug(u'{} unchanged'.format(self.get_short_dn(dn)))
|
---|
| 356 |
|
---|
[1221] | 357 | def notify_created(self, dn, ok=True):
|
---|
| 358 | if ok:
|
---|
[1259] | 359 | logger.debug(u'{} created'.format(self.get_short_dn(dn)))
|
---|
[1221] | 360 | self.result['add']['ok'].append(dn)
|
---|
| 361 | else:
|
---|
[1259] | 362 | self.logger.warning(u"failed to add {}".format(dn))
|
---|
[1221] | 363 | self.result['add']['failed'].append(dn)
|
---|
[1219] | 364 |
|
---|
[1221] | 365 | def notify_modified(self, dn, ok=True):
|
---|
| 366 | if ok:
|
---|
[1259] | 367 | logger.debug(u'{} modified'.format(self.get_short_dn(dn)))
|
---|
[1221] | 368 | self.result['update']['ok'].append(dn)
|
---|
| 369 | else:
|
---|
[1259] | 370 | self.logger.error(u"failed to modify {}".format(dn))
|
---|
[1221] | 371 | self.result['update']['failed'].append(dn)
|
---|
[1219] | 372 |
|
---|
[1221] | 373 | def notify_deleted(self, dn, ok=True):
|
---|
| 374 | if ok:
|
---|
[1259] | 375 | logger.debug(u'{} deleted'.format(self.get_short_dn(dn)))
|
---|
[1221] | 376 | self.result['delete']['ok'].append(dn)
|
---|
| 377 | else:
|
---|
[1259] | 378 | self.logger.error(u"failed to delete {}".format(dn))
|
---|
[1221] | 379 | self.result['delete']['failed'].append(dn)
|
---|
[1219] | 380 |
|
---|
| 381 | def notify_renamed(self, dn, newdn, uid, newuid, options):
|
---|
[1259] | 382 | print(u"renamed {} -> {}".format(dn, newdn))
|
---|
[1221] | 383 | subprocess.check_call(
|
---|
| 384 | "%s %s %s %s %s" % (options.renamecommand, dn, newdn, uid, newuid),
|
---|
| 385 | shell=True)
|
---|
[1219] | 386 |
|
---|
| 387 |
|
---|
[1221] | 388 |
|
---|
| 389 | class SyncReplConsumer(ReconnectLDAPObject, SyncreplConsumer):
|
---|
| 390 | """
|
---|
| 391 | Syncrepl Consumer interface
|
---|
| 392 | """
|
---|
| 393 |
|
---|
| 394 | def __init__(self, dest, syncrepl_entry_callback, *args, **kwargs):
|
---|
| 395 | self.logger = logging.getLogger()
|
---|
| 396 | # Initialise the LDAP Connection first
|
---|
| 397 | ldap.ldapobject.ReconnectLDAPObject.__init__(self, *args, **kwargs)
|
---|
| 398 | # We need this for later internal use
|
---|
| 399 | self.__presentUUIDs = dict()
|
---|
| 400 | self.cookie = None
|
---|
| 401 | self.dest_ldap = dest
|
---|
| 402 | self.syncrepl_entry_callback = syncrepl_entry_callback
|
---|
| 403 |
|
---|
| 404 | def syncrepl_get_cookie(self):
|
---|
| 405 | return self.cookie
|
---|
| 406 |
|
---|
| 407 | def syncrepl_set_cookie(self, cookie):
|
---|
| 408 | self.cookie = cookie
|
---|
| 409 |
|
---|
| 410 | def syncrepl_entry(self, dn, attributes, uuid):
|
---|
| 411 | # First we determine the type of change we have here
|
---|
| 412 | # (and store away the previous data for later if needed)
|
---|
| 413 | if uuid in self.__presentUUIDs:
|
---|
| 414 | change_type = 'modify'
|
---|
| 415 | else:
|
---|
| 416 | change_type = 'add'
|
---|
| 417 | # Now we store our knowledge of the existence of this entry
|
---|
| 418 | self.__presentUUIDs[uuid] = dn
|
---|
| 419 | # Debugging
|
---|
| 420 | logger.debug('{}: {} ({})'.format(dn, change_type, ",".join(attributes.keys())))
|
---|
| 421 | # If we have a cookie then this is not our first time being run,
|
---|
| 422 | # so it must be a change
|
---|
| 423 | if self.cookie is not None:
|
---|
| 424 | self.syncrepl_entry_callback(dn, attributes)
|
---|
| 425 |
|
---|
| 426 |
|
---|
| 427 | def syncrepl_delete(self, uuids):
|
---|
| 428 | """ syncrepl_delete """
|
---|
| 429 | # Make sure we know about the UUID being deleted, just in case...
|
---|
| 430 | uuids = [uuid for uuid in uuids if uuid in self.__presentUUIDs]
|
---|
| 431 | # Delete all the UUID values we know of
|
---|
| 432 | for uuid in uuids:
|
---|
| 433 | logger.debug('detected deletion of entry {} ({})', uuid, self.__presentUUIDs[uuid])
|
---|
| 434 | del self.__presentUUIDs[uuid]
|
---|
| 435 |
|
---|
| 436 | def syncrepl_present(self, uuids, refreshDeletes=False):
|
---|
| 437 | """ called on initial sync """
|
---|
| 438 | if uuids is not None:
|
---|
| 439 | self.logger.debug('uuids: {}'.format(','.join(uuids)))
|
---|
| 440 | # If we have not been given any UUID values,
|
---|
| 441 | # then we have recieved all the present controls...
|
---|
| 442 | if uuids is None:
|
---|
| 443 | # We only do things if refreshDeletes is false as the syncrepl
|
---|
| 444 | # extension will call syncrepl_delete instead when it detects a
|
---|
| 445 | # delete notice
|
---|
| 446 | if not refreshDeletes:
|
---|
| 447 | deletedEntries = [
|
---|
| 448 | uuid for uuid in self.__presentUUIDs
|
---|
| 449 | ]
|
---|
| 450 | self.syncrepl_delete(deletedEntries)
|
---|
| 451 | # Phase is now completed, reset the list
|
---|
| 452 | self.__presentUUIDs = {}
|
---|
| 453 | else:
|
---|
| 454 | # Note down all the UUIDs we have been sent
|
---|
| 455 | for uuid in uuids:
|
---|
| 456 | self.__presentUUIDs[uuid] = True
|
---|
| 457 |
|
---|
| 458 |
|
---|
| 459 | def syncrepl_refreshdone(self):
|
---|
| 460 | self.logger.info('Initial synchronization is now done, persist phase begins')
|
---|
| 461 | #self.logger.debug('UUIDs:\n' + '\n'.join(self.__presentUUIDs))
|
---|
| 462 |
|
---|
| 463 |
|
---|
| 464 |
|
---|
| 465 | class LdapSyncRepl(LdapSync):
|
---|
| 466 | def __init__(self, destsrv,
|
---|
| 467 | destadmindn, destadminpw,
|
---|
| 468 | basedn, destbasedn,
|
---|
| 469 | options=Options(), source_ldap_url_obj=None):
|
---|
| 470 | # Install our signal handlers
|
---|
| 471 | signal.signal(signal.SIGTERM, self.shutdown)
|
---|
| 472 | self.watcher_running = False
|
---|
| 473 | self.source_ldap_url_obj = source_ldap_url_obj
|
---|
| 474 | self.ldap_credentials = False
|
---|
| 475 | self.source_ldap_connection = None
|
---|
| 476 | super(LdapSyncRepl, self).__init__(destsrv,
|
---|
| 477 | destadmindn, destadminpw,
|
---|
| 478 | basedn, destbasedn, options)
|
---|
| 479 |
|
---|
| 480 |
|
---|
| 481 | def sync(self):
|
---|
| 482 | self._dest_ldap_connect()
|
---|
| 483 | self.watcher_running = True
|
---|
| 484 | while self.watcher_running:
|
---|
| 485 | self.logger.info('Connecting to source LDAP server')
|
---|
| 486 | # Prepare the LDAP server connection (triggers the connection as well)
|
---|
| 487 | self.source_ldap_connection = SyncReplConsumer(self.con,
|
---|
| 488 | self.perform_application_sync_callback,
|
---|
| 489 | self.source_ldap_url_obj.initializeUrl())
|
---|
| 490 |
|
---|
| 491 | if self.source_ldap_url_obj.who and self.source_ldap_url_obj.cred:
|
---|
| 492 | self.ldap_credentials = True
|
---|
| 493 | # Now we login to the LDAP server
|
---|
| 494 | try:
|
---|
| 495 | self.source_ldap_connection.simple_bind_s(
|
---|
| 496 | self.source_ldap_url_obj.who, self.source_ldap_url_obj.cred)
|
---|
[1259] | 497 | except ldap.INVALID_CREDENTIALS as e:
|
---|
| 498 | print('Login to LDAP server failed: ', str(e))
|
---|
[1221] | 499 | sys.exit(1)
|
---|
| 500 | except ldap.SERVER_DOWN:
|
---|
[1259] | 501 | print('LDAP server is down, going to retry.')
|
---|
[1221] | 502 | time.sleep(5)
|
---|
| 503 | continue
|
---|
| 504 |
|
---|
| 505 | # Commence the syncing
|
---|
| 506 | self.logger.info('Staring sync process')
|
---|
| 507 | ldap_search = self.source_ldap_connection.syncrepl_search(
|
---|
| 508 | self.source_ldap_url_obj.dn or '',
|
---|
| 509 | self.source_ldap_url_obj.scope or ldap.SCOPE_SUBTREE,
|
---|
| 510 | mode='refreshAndPersist',
|
---|
| 511 | attrlist=self.source_ldap_url_obj.attrs,
|
---|
| 512 | filterstr=self.source_ldap_url_obj.filterstr or '(objectClass=*)'
|
---|
| 513 | )
|
---|
| 514 |
|
---|
| 515 | try:
|
---|
| 516 | while self.source_ldap_connection.syncrepl_poll(all=1, msgid=ldap_search):
|
---|
[1259] | 517 | print(".", end="")
|
---|
[1221] | 518 | except KeyboardInterrupt:
|
---|
| 519 | # User asked to exit
|
---|
[1259] | 520 | print("aborted\n")
|
---|
[1221] | 521 | self.shutdown(None, None)
|
---|
[1259] | 522 | except Exception as e:
|
---|
[1221] | 523 | # Handle any exception
|
---|
| 524 | if self.watcher_running:
|
---|
| 525 | self.logger.exception('Encountered a problem, going to retry.')
|
---|
| 526 | time.sleep(5)
|
---|
| 527 |
|
---|
| 528 | def perform_application_sync_callback(self, dn, attributes):
|
---|
| 529 | logger.debug('{}: src: {}'.format(dn, str(attributes)))
|
---|
| 530 | try:
|
---|
| 531 | self._syncLdapObject(dn, attributes)
|
---|
| 532 | except ldap.NO_SUCH_OBJECT:
|
---|
| 533 | self.logger.info("SKIPPED: {} object does not exist on target".format(dn))
|
---|
| 534 | return False
|
---|
| 535 | return True
|
---|
| 536 |
|
---|
| 537 | def shutdown(self, signum, stack):
|
---|
| 538 | # Declare the needed global variables
|
---|
| 539 | self.logger.info('Shutting down!')
|
---|
| 540 |
|
---|
| 541 | # We are no longer running
|
---|
| 542 | self.watcher_running = False
|
---|
| 543 |
|
---|
[1259] | 544 | def get_ldap_url_obj(self, configsection):
|
---|
| 545 | baseurl = 'ldap://{server}:389/{basedn}'.format(server=configsection.get('server'), basedn=configsection.get('basedn'))
|
---|
| 546 | attrs = None
|
---|
| 547 | if configsection.get('attributes') is not None:
|
---|
| 548 | attrs = configsection.get('attributes').split(',')
|
---|
| 549 | return LDAPUrl(
|
---|
| 550 | baseurl,
|
---|
| 551 | dn=configsection.get('baseDn'),
|
---|
| 552 | who=configsection.get('bindDn'),
|
---|
| 553 | cred=configsection.get('basePassword'),
|
---|
| 554 | filterstr=configsection.get('filter'),
|
---|
| 555 | attrs=attrs
|
---|
| 556 | )
|
---|
[1221] | 557 |
|
---|
[1259] | 558 |
|
---|
[1218] | 559 | if __name__ == "__main__":
|
---|
[1219] | 560 | logging.basicConfig(format='%(levelname)s %(module)s.%(funcName)s: %(message)s', level=logging.INFO)
|
---|
| 561 | logger = logging.getLogger()
|
---|
| 562 |
|
---|
[1221] | 563 | args = getArguments()
|
---|
[1219] | 564 | if args.debug:
|
---|
| 565 | logger.setLevel(logging.DEBUG)
|
---|
[1221] | 566 | conffile = args.configfile
|
---|
[1219] | 567 |
|
---|
[1218] | 568 | exclude = None
|
---|
| 569 |
|
---|
[1259] | 570 | config = ConfigParser()
|
---|
[1218] | 571 | config.read(conffile)
|
---|
| 572 |
|
---|
| 573 | srcfile = None
|
---|
| 574 | try:
|
---|
[1221] | 575 | srcfile = config.get("source", "file")
|
---|
[1218] | 576 | except:
|
---|
| 577 | pass
|
---|
| 578 |
|
---|
[1221] | 579 | basedn = config.get("source", "baseDn")
|
---|
[1259] | 580 | filterstr = config.get("source", "filter", fallback=None)
|
---|
[1218] | 581 |
|
---|
[1221] | 582 | if srcfile is None:
|
---|
| 583 | srv = config.get("source", "server")
|
---|
| 584 | admindn = config.get("source", "bindDn")
|
---|
| 585 | adminpw = config.get("source", "bindPassword")
|
---|
| 586 | starttls = config.getboolean("source", "starttls")
|
---|
[1218] | 587 |
|
---|
[1221] | 588 | destsrv = config.get("destination", "server")
|
---|
| 589 | destadmindn = config.get("destination", "bindDn")
|
---|
| 590 | destadminpw = config.get("destination", "bindPassword")
|
---|
| 591 | destbasedn = config.get("destination", "baseDn")
|
---|
[1218] | 592 | try:
|
---|
[1221] | 593 | rdn = config.get("destination", "rdn")
|
---|
[1219] | 594 | logger.warning("setting rdn is currently ignored")
|
---|
[1218] | 595 | except:
|
---|
[1219] | 596 | pass
|
---|
[1218] | 597 |
|
---|
[1219] | 598 | options = Options()
|
---|
[1218] | 599 | try:
|
---|
[1221] | 600 | options.exclude = config.get("destination", "excludesubtree").lower()
|
---|
[1218] | 601 | except:
|
---|
[1219] | 602 | pass
|
---|
[1218] | 603 |
|
---|
[1259] | 604 | options.updateonly = not config.getboolean("destination", "create", fallback=False)
|
---|
| 605 | options.delete = config.getboolean("destination", "delete", fallback=False)
|
---|
| 606 | options.starttls = config.getboolean("destination", "starttls", fallback=False)
|
---|
| 607 | options.renameattr = config.get("destination", "detectRename", fallback=None)
|
---|
| 608 | options.renamecommand = config.get("destination", "detectRename", fallback=None)
|
---|
| 609 | options.pwd_max_days = int(config.get("source", "pwd_max_days", fallback=0))
|
---|
[1221] | 610 | options.filter = filterstr
|
---|
[1219] | 611 |
|
---|
[1221] | 612 | # Set source.attrlist as global option.
|
---|
| 613 | # If source would use less attributes than dest,
|
---|
| 614 | # all attributes not retrieved from source would be deleted from dest
|
---|
[1218] | 615 | try:
|
---|
[1221] | 616 | options.attrlist = config.get("source", "attributes").split(",")
|
---|
[1218] | 617 | except:
|
---|
[1221] | 618 | options.attrlist = None
|
---|
[1218] | 619 |
|
---|
[1259] | 620 | if config.get('source', 'mode', fallback=None) == 'syncrepl':
|
---|
[1221] | 621 | ldapsync = LdapSyncRepl(
|
---|
| 622 | destsrv, destadmindn, destadminpw, basedn, destbasedn,
|
---|
| 623 | options,
|
---|
[1259] | 624 | source_ldap_url_obj=get_ldap_url_obj(config['source']))
|
---|
[1221] | 625 | ldapsync.sync()
|
---|
[1218] | 626 | else:
|
---|
[1221] | 627 | if srcfile:
|
---|
| 628 | objects = readLDIFSource(srcfile)
|
---|
| 629 | else:
|
---|
| 630 | objects = readLdapSource(srv, admindn, adminpw,
|
---|
| 631 | basedn, filterstr, options.attrlist, starttls)
|
---|
[1218] | 632 |
|
---|
[1221] | 633 | ldapsync = LdapSync(destsrv, destadmindn, destadminpw, basedn, destbasedn, options)
|
---|
| 634 | ldapsync.sync(objects)
|
---|