diff options
Diffstat (limited to 'ipaserver/install/replication.py')
-rw-r--r-- | ipaserver/install/replication.py | 532 |
1 files changed, 532 insertions, 0 deletions
diff --git a/ipaserver/install/replication.py b/ipaserver/install/replication.py new file mode 100644 index 000000000..8477bd18a --- /dev/null +++ b/ipaserver/install/replication.py @@ -0,0 +1,532 @@ +# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com> +# +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import time, logging + +import ipaldap, ldap, dsinstance +from ldap import modlist +from ipa import ipaerror + +DIRMAN_CN = "cn=directory manager" +CACERT="/usr/share/ipa/html/ca.crt" +# the default container used by AD for user entries +WIN_USER_CONTAINER="cn=Users" +# the default container used by IPA for user entries +IPA_USER_CONTAINER="cn=users,cn=accounts" +PORT = 636 +TIMEOUT = 120 + +IPA_REPLICA = 1 +WINSYNC = 2 + +class ReplicationManager: + """Manage replication agreements between DS servers, and sync + agreements with Windows servers""" + def __init__(self, hostname, dirman_passwd): + self.hostname = hostname + self.dirman_passwd = dirman_passwd + + self.conn = ipaldap.IPAdmin(hostname, port=PORT, cacert=CACERT) + self.conn.do_simple_bind(bindpw=dirman_passwd) + + self.repl_man_passwd = dirman_passwd + + # these are likely constant, but you could change them + # at runtime if you really want + self.repl_man_dn = "cn=replication manager,cn=config" + self.repl_man_cn = "replication manager" + self.suffix = "" + + def _get_replica_id(self, conn, master_conn): + """ + Returns the replica ID which is unique for each backend. + + conn is the connection we are trying to get the replica ID for. + master_conn is the master we are going to replicate with. + """ + # First see if there is already one set + dn = self.replica_dn() + try: + replica = conn.search_s(dn, ldap.SCOPE_BASE, "objectclass=*")[0] + if replica.getValue('nsDS5ReplicaId'): + return int(replica.getValue('nsDS5ReplicaId')) + except ldap.NO_SUCH_OBJECT: + pass + + # Ok, either the entry doesn't exist or the attribute isn't set + # so get it from the other master + retval = -1 + dn = "cn=replication, cn=etc, %s" % self.suffix + try: + replica = master_conn.search_s(dn, ldap.SCOPE_BASE, "objectclass=*")[0] + if not replica.getValue('nsDS5ReplicaId'): + logging.debug("Unable to retrieve nsDS5ReplicaId from remote server") + raise RuntimeError("Unable to retrieve nsDS5ReplicaId from remote server") + except ldap.NO_SUCH_OBJECT: + logging.debug("Unable to retrieve nsDS5ReplicaId from remote server") + raise + + # Now update the value on the master + retval = int(replica.getValue('nsDS5ReplicaId')) + mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaId', str(retval + 1))] + + try: + master_conn.modify_s(dn, mod) + except Exception, e: + logging.debug("Problem updating nsDS5ReplicaID %s" % e) + raise + + return retval + + def find_replication_dns(self, conn): + filt = "(|(objectclass=nsDSWindowsReplicationAgreement)(objectclass=nsds5ReplicationAgreement))" + try: + ents = conn.search_s("cn=mapping tree,cn=config", ldap.SCOPE_SUBTREE, filt) + except ldap.NO_SUCH_OBJECT: + return [] + return [ent.dn for ent in ents] + + def add_replication_manager(self, conn, passwd=None): + """ + Create a pseudo user to use for replication. If no password + is provided the directory manager password will be used. + """ + + if passwd: + self.repl_man_passwd = passwd + + ent = ipaldap.Entry(self.repl_man_dn) + ent.setValues("objectclass", "top", "person") + ent.setValues("cn", self.repl_man_cn) + ent.setValues("userpassword", self.repl_man_passwd) + ent.setValues("sn", "replication manager pseudo user") + + try: + conn.add_s(ent) + except ldap.ALREADY_EXISTS: + # should we set the password here? + pass + + def delete_replication_manager(self, conn, dn="cn=replication manager,cn=config"): + try: + conn.delete_s(dn) + except ldap.NO_SUCH_OBJECT: + pass + + def get_replica_type(self, master=True): + if master: + return "3" + else: + return "2" + + def replica_dn(self): + return 'cn=replica, cn="%s", cn=mapping tree, cn=config' % self.suffix + + def local_replica_config(self, conn, replica_id): + dn = self.replica_dn() + + try: + conn.getEntry(dn, ldap.SCOPE_BASE) + # replication is already configured + return + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + pass + + replica_type = self.get_replica_type() + + entry = ipaldap.Entry(dn) + entry.setValues('objectclass', "top", "nsds5replica", "extensibleobject") + entry.setValues('cn', "replica") + entry.setValues('nsds5replicaroot', self.suffix) + entry.setValues('nsds5replicaid', str(replica_id)) + entry.setValues('nsds5replicatype', replica_type) + entry.setValues('nsds5flags', "1") + entry.setValues('nsds5replicabinddn', [self.repl_man_dn]) + entry.setValues('nsds5replicalegacyconsumer', "off") + + conn.add_s(entry) + + def setup_changelog(self, conn): + dn = "cn=changelog5, cn=config" + dirpath = conn.dbdir + "/cldb" + entry = ipaldap.Entry(dn) + entry.setValues('objectclass', "top", "extensibleobject") + entry.setValues('cn', "changelog5") + entry.setValues('nsslapd-changelogdir', dirpath) + try: + conn.add_s(entry) + except ldap.ALREADY_EXISTS: + return + + def setup_chaining_backend(self, conn): + chaindn = "cn=chaining database, cn=plugins, cn=config" + benamebase = "chaindb" + urls = [self.to_ldap_url(conn)] + cn = "" + benum = 1 + done = False + while not done: + try: + cn = benamebase + str(benum) # e.g. localdb1 + dn = "cn=" + cn + ", " + chaindn + entry = ipaldap.Entry(dn) + entry.setValues('objectclass', 'top', 'extensibleObject', 'nsBackendInstance') + entry.setValues('cn', cn) + entry.setValues('nsslapd-suffix', self.suffix) + entry.setValues('nsfarmserverurl', urls) + entry.setValues('nsmultiplexorbinddn', self.repl_man_dn) + entry.setValues('nsmultiplexorcredentials', self.repl_man_passwd) + + self.conn.add_s(entry) + done = True + except ldap.ALREADY_EXISTS: + benum += 1 + except ldap.LDAPError, e: + print "Could not add backend entry " + dn, e + raise + + return cn + + def to_ldap_url(self, conn): + return "ldap://%s:%d/" % (conn.host, conn.port) + + def setup_chaining_farm(self, conn): + try: + conn.modify_s(self.suffix, [(ldap.MOD_ADD, 'aci', + [ "(targetattr = \"*\")(version 3.0; acl \"Proxied authorization for database links\"; allow (proxy) userdn = \"ldap:///%s\";)" % self.repl_man_dn ])]) + except ldap.TYPE_OR_VALUE_EXISTS: + logging.debug("proxy aci already exists in suffix %s on %s" % (self.suffix, conn.host)) + + def get_mapping_tree_entry(self): + try: + entry = self.conn.getEntry("cn=mapping tree,cn=config", ldap.SCOPE_ONELEVEL, + "(cn=\"%s\")" % (self.suffix)) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND), e: + logging.debug("failed to find mappting tree entry for %s" % self.suffix) + raise e + + return entry + + + def enable_chain_on_update(self, bename): + mtent = self.get_mapping_tree_entry() + dn = mtent.dn + + plgent = self.conn.getEntry("cn=Multimaster Replication Plugin,cn=plugins,cn=config", + ldap.SCOPE_BASE, "(objectclass=*)", ['nsslapd-pluginPath']) + path = plgent.getValue('nsslapd-pluginPath') + + mod = [(ldap.MOD_REPLACE, 'nsslapd-state', 'backend'), + (ldap.MOD_ADD, 'nsslapd-backend', bename), + (ldap.MOD_ADD, 'nsslapd-distribution-plugin', path), + (ldap.MOD_ADD, 'nsslapd-distribution-funct', 'repl_chain_on_update')] + + try: + self.conn.modify_s(dn, mod) + except ldap.TYPE_OR_VALUE_EXISTS: + logging.debug("chainOnUpdate already enabled for %s" % self.suffix) + + def setup_chain_on_update(self, other_conn): + chainbe = self.setup_chaining_backend(other_conn) + self.enable_chain_on_update(chainbe) + + def add_passsync_user(self, conn, password): + pass_dn = "uid=passsync,cn=sysaccounts,cn=etc,%s" % self.suffix + print "The user for the Windows PassSync service is %s" % pass_dn + try: + conn.getEntry(pass_dn, ldap.SCOPE_BASE) + print "Windows PassSync entry exists, not resetting password" + return + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + pass + + # The user doesn't exist, add it + entry = ipaldap.Entry(pass_dn) + entry.setValues("objectclass", ["account", "simplesecurityobject"]) + entry.setValues("uid", "passsync") + entry.setValues("userPassword", password) + conn.add_s(entry) + + # Add it to the list of users allowed to bypass password policy + extop_dn = "cn=ipa_pwd_extop,cn=plugins,cn=config" + entry = conn.getEntry(extop_dn, ldap.SCOPE_BASE) + pass_mgrs = entry.getValues('passSyncManagersDNs') + if not pass_mgrs: + pass_mgrs = [] + if not isinstance(pass_mgrs, list): + pass_mgrs = [pass_mgrs] + pass_mgrs.append(pass_dn) + mod = [(ldap.MOD_REPLACE, 'passSyncManagersDNs', pass_mgrs)] + conn.modify_s(extop_dn, mod) + + # And finally grant it permission to write passwords + mod = [(ldap.MOD_ADD, 'aci', + ['(targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Windows PassSync service can write passwords"; allow (write) userdn="ldap:///%s";)' % pass_dn])] + try: + conn.modify_s(self.suffix, mod) + except ldap.TYPE_OR_VALUE_EXISTS: + logging.debug("passsync aci already exists in suffix %s on %s" % (self.suffix, conn.host)) + + def setup_winsync_agmt(self, entry, **kargs): + entry.setValues("objectclass", "nsDSWindowsReplicationAgreement") + entry.setValues("nsds7WindowsReplicaSubtree", + kargs.get("win_subtree", + WIN_USER_CONTAINER + "," + self.suffix)) + entry.setValues("nsds7DirectoryReplicaSubtree", + kargs.get("ds_subtree", + IPA_USER_CONTAINER + "," + self.suffix)) + # for now, just sync users and ignore groups + entry.setValues("nsds7NewWinUserSyncEnabled", kargs.get('newwinusers', 'true')) + entry.setValues("nsds7NewWinGroupSyncEnabled", kargs.get('newwingroups', 'false')) + windomain = '' + if kargs.has_key('windomain'): + windomain = kargs['windomain'] + else: + windomain = '.'.join(ldap.explode_dn(self.suffix, 1)) + entry.setValues("nsds7WindowsDomain", windomain) + + def agreement_dn(self, hostname, port=PORT): + cn = "meTo%s%d" % (hostname, port) + dn = "cn=%s, %s" % (cn, self.replica_dn()) + + return (cn, dn) + + def setup_agreement(self, a, b, **kargs): + cn, dn = self.agreement_dn(b.host) + try: + a.getEntry(dn, ldap.SCOPE_BASE) + return + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + pass + + iswinsync = kargs.get("winsync", False) + repl_man_dn = kargs.get("binddn", self.repl_man_dn) + repl_man_passwd = kargs.get("bindpw", self.repl_man_passwd) + port = kargs.get("port", PORT) + + entry = ipaldap.Entry(dn) + entry.setValues('objectclass', "nsds5replicationagreement") + entry.setValues('cn', cn) + entry.setValues('nsds5replicahost', b.host) + entry.setValues('nsds5replicaport', str(port)) + entry.setValues('nsds5replicatimeout', str(TIMEOUT)) + entry.setValues('nsds5replicabinddn', repl_man_dn) + entry.setValues('nsds5replicacredentials', repl_man_passwd) + entry.setValues('nsds5replicabindmethod', 'simple') + entry.setValues('nsds5replicaroot', self.suffix) + entry.setValues('nsds5replicaupdateschedule', '0000-2359 0123456') + entry.setValues('nsds5replicatransportinfo', 'SSL') + entry.setValues('nsDS5ReplicatedAttributeList', '(objectclass=*) $ EXCLUDE memberOf') + entry.setValues('description', "me to %s%d" % (b.host, port)) + if iswinsync: + self.setup_winsync_agmt(entry, **kargs) + + a.add_s(entry) + + entry = a.waitForEntry(entry) + + def delete_agreement(self, hostname): + cn, dn = self.agreement_dn(hostname) + return self.conn.deleteEntry(dn) + + def check_repl_init(self, conn, agmtdn): + done = False + hasError = 0 + attrlist = ['cn', 'nsds5BeginReplicaRefresh', 'nsds5replicaUpdateInProgress', + 'nsds5ReplicaLastInitStatus', 'nsds5ReplicaLastInitStart', + 'nsds5ReplicaLastInitEnd'] + entry = conn.getEntry(agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist) + if not entry: + print "Error reading status from agreement", agmtdn + hasError = 1 + else: + refresh = entry.nsds5BeginReplicaRefresh + inprogress = entry.nsds5replicaUpdateInProgress + status = entry.nsds5ReplicaLastInitStatus + if not refresh: # done - check status + if not status: + print "No status yet" + elif status.find("replica busy") > -1: + print "[%s] reports: Replica Busy! Status: [%s]" % (conn.host, status) + done = True + hasError = 2 + elif status.find("Total update succeeded") > -1: + print "Update succeeded" + done = True + elif inprogress.lower() == 'true': + print "Update in progress yet not in progress" + else: + print "[%s] reports: Update failed! Status: [%s]" % (conn.host, status) + hasError = 1 + done = True + else: + print "Update in progress" + + return done, hasError + + def check_repl_update(self, conn, agmtdn): + done = False + hasError = 0 + attrlist = ['cn', 'nsds5replicaUpdateInProgress', + 'nsds5ReplicaLastUpdateStatus', 'nsds5ReplicaLastUpdateStart', + 'nsds5ReplicaLastUpdateEnd'] + entry = conn.getEntry(agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist) + if not entry: + print "Error reading status from agreement", agmtdn + hasError = 1 + else: + inprogress = entry.nsds5replicaUpdateInProgress + status = entry.nsds5ReplicaLastUpdateStatus + start = entry.nsds5ReplicaLastUpdateStart + end = entry.nsds5ReplicaLastUpdateEnd + # incremental update is done if inprogress is false and end >= start + done = inprogress and inprogress.lower() == 'false' and start and end and (start <= end) + logging.info("Replication Update in progress: %s: status: %s: start: %s: end: %s" % + (inprogress, status, start, end)) + if not done and status: # check for errors + # status will usually be a number followed by a string + # number != 0 means error + rc, msg = status.split(' ', 1) + if rc != '0': + hasError = 1 + done = True + + return done, hasError + + def wait_for_repl_init(self, conn, agmtdn): + done = False + haserror = 0 + while not done and not haserror: + time.sleep(1) # give it a few seconds to get going + done, haserror = self.check_repl_init(conn, agmtdn) + return haserror + + def wait_for_repl_update(self, conn, agmtdn, maxtries=600): + done = False + haserror = 0 + while not done and not haserror and maxtries > 0: + time.sleep(1) # give it a few seconds to get going + done, haserror = self.check_repl_update(conn, agmtdn) + maxtries -= 1 + if maxtries == 0: # too many tries + print "Error: timeout: could not determine agreement status: please check your directory server logs for possible errors" + haserror = 1 + return haserror + + def start_replication(self, other_conn, conn=None): + print "Starting replication, please wait until this has completed." + if conn == None: + conn = self.conn + cn, dn = self.agreement_dn(conn.host) + + mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')] + other_conn.modify_s(dn, mod) + + return self.wait_for_repl_init(other_conn, dn) + + def basic_replication_setup(self, conn, replica_id): + self.add_replication_manager(conn) + self.local_replica_config(conn, replica_id) + self.setup_changelog(conn) + + def setup_replication(self, other_hostname, realm_name, **kargs): + """ + NOTES: + - the directory manager password needs to be the same on + both directories. Or use the optional binddn and bindpw + """ + iswinsync = kargs.get("winsync", False) + oth_port = kargs.get("port", PORT) + oth_cacert = kargs.get("cacert", CACERT) + oth_binddn = kargs.get("binddn", DIRMAN_CN) + oth_bindpw = kargs.get("bindpw", self.dirman_passwd) + # note - there appears to be a bug in python-ldap - it does not + # allow connections using two different CA certs + other_conn = ipaldap.IPAdmin(other_hostname, port=oth_port, cacert=oth_cacert) + try: + other_conn.do_simple_bind(binddn=oth_binddn, bindpw=oth_bindpw) + except Exception, e: + if iswinsync: + logging.info("Could not validate connection to remote server %s:%d - continuing" % + (other_hostname, oth_port)) + logging.info("The error was: %s" % e) + else: + raise e + + self.suffix = ipaldap.IPAdmin.normalizeDN(dsinstance.realm_to_suffix(realm_name)) + + if not iswinsync: + local_id = self._get_replica_id(self.conn, other_conn) + else: + # there is no other side to get a replica ID from + local_id = self._get_replica_id(self.conn, self.conn) + self.basic_replication_setup(self.conn, local_id) + + if not iswinsync: + other_id = self._get_replica_id(other_conn, other_conn) + self.basic_replication_setup(other_conn, other_id) + self.setup_agreement(other_conn, self.conn) + self.setup_agreement(self.conn, other_conn) + return self.start_replication(other_conn) + else: + self.add_passsync_user(self.conn, kargs.get("passsync")) + self.setup_agreement(self.conn, other_conn, **kargs) + logging.info("Added new sync agreement, waiting for it to become ready . . .") + cn, dn = self.agreement_dn(other_hostname) + self.wait_for_repl_update(self.conn, dn, 30) + logging.info("Agreement is ready, starting replication . . .") + return self.start_replication(self.conn, other_conn) + + def initialize_replication(self, dn, conn): + mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')] + try: + conn.modify_s(dn, mod) + except ldap.ALREADY_EXISTS: + return + + def force_synch(self, dn, schedule, conn): + newschedule = '2358-2359 0' + + # On the remote chance of a match. We force a synch to happen right + # now by changing the schedule to something else and quickly changing + # it back. + if newschedule == schedule: + newschedule = '2358-2359 1' + logging.info("Changing agreement %s schedule to %s to force synch" % + (dn, newschedule)) + mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaUpdateSchedule', [ newschedule ])] + conn.modify_s(dn, mod) + time.sleep(1) + logging.info("Changing agreement %s to restore original schedule %s" % + (dn, schedule)) + mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaUpdateSchedule', [ schedule ])] + conn.modify_s(dn, mod) + + def get_agreement_type(self, hostname): + cn, dn = self.agreement_dn(hostname) + + entry = self.conn.getEntry(dn, ldap.SCOPE_BASE) + + objectclass = entry.getValues("objectclass") + + for o in objectclass: + if o.lower() == "nsdswindowsreplicationagreement": + return WINSYNC + + return IPA_REPLICA |