From 3fdca99c48f19d6af7182b69bea0ee11100a9dd7 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 14 Jul 2011 23:35:01 -0400 Subject: Create tool to manage dogtag replication agreements For the most part the existing replication code worked with the following exceptions: - Added more port options - It assumed that initial connections were done to an SSL port. Added ability to use startTLS - It assumed that the name of the agreement was the same on both sides. In dogtag one is marked as master and one as clone. A new option is added, master, the determines which side we're working on or None if it isn't a dogtag agreement. - Don't set the attribute exclude list on dogtag agreements - dogtag doesn't set a schedule by default (which is actually recommended by 389-ds). This causes problems when doing a force-sync though so if one is done we set a schedule to run all the time. Otherwise the temporary schedule can't be removed (LDAP operations error). https://fedorahosted.org/freeipa/ticket/1250 --- freeipa.spec.in | 7 +- install/tools/Makefile.am | 1 + install/tools/ipa-csreplica-manage | 452 +++++++++++++++++++++++++++++++ install/tools/man/Makefile.am | 1 + install/tools/man/ipa-csreplica-manage.1 | 93 +++++++ ipaserver/install/dsinstance.py | 4 +- ipaserver/install/replication.py | 92 +++++-- 7 files changed, 620 insertions(+), 30 deletions(-) create mode 100755 install/tools/ipa-csreplica-manage create mode 100644 install/tools/man/ipa-csreplica-manage.1 diff --git a/freeipa.spec.in b/freeipa.spec.in index 276001ae6..2cbfed864 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -365,6 +365,7 @@ fi %{_sbindir}/ipa-replica-install %{_sbindir}/ipa-replica-prepare %{_sbindir}/ipa-replica-manage +%{_sbindir}/ipa-csreplica-manage %{_sbindir}/ipa-server-certinstall %{_sbindir}/ipa-ldap-updater %{_sbindir}/ipa-compat-manage @@ -437,6 +438,7 @@ fi %{_mandir}/man1/ipa-replica-conncheck.1.gz %{_mandir}/man1/ipa-replica-install.1.gz %{_mandir}/man1/ipa-replica-manage.1.gz +%{_mandir}/man1/ipa-csreplica-manage.1.gz %{_mandir}/man1/ipa-replica-prepare.1.gz %{_mandir}/man1/ipa-server-certinstall.1.gz %{_mandir}/man1/ipa-server-install.1.gz @@ -504,7 +506,10 @@ fi %ghost %attr(0644,root,apache) %config(noreplace) %{_sysconfdir}/ipa/default.conf %changelog -* Wed Jul 6 2011 Adam Young - 2.0.90-5 +* Thu Jul 14 2011 Rob Crittenden - 2.0.90-6 +- Add ipa-csreplica-manage tool. + +* Wed Jul 6 2011 Adam Young - 2.0.90-5 - Add HTML file describing issues with HBAC deny rules * Fri Jun 17 2011 Rob Crittenden - 2.0.90-4 diff --git a/install/tools/Makefile.am b/install/tools/Makefile.am index c6ecd9287..fc615ec04 100644 --- a/install/tools/Makefile.am +++ b/install/tools/Makefile.am @@ -12,6 +12,7 @@ sbin_SCRIPTS = \ ipa-replica-install \ ipa-replica-prepare \ ipa-replica-manage \ + ipa-csreplica-manage \ ipa-server-certinstall \ ipactl \ ipa-compat-manage \ diff --git a/install/tools/ipa-csreplica-manage b/install/tools/ipa-csreplica-manage new file mode 100755 index 000000000..39d505654 --- /dev/null +++ b/install/tools/ipa-csreplica-manage @@ -0,0 +1,452 @@ +#! /usr/bin/python -E +# Authors: Rob Crittenden +# +# Based on ipa-replica-manage by Karl MacMillan +# +# Copyright (C) 2011 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see . +# +import sys +import os + +import getpass, ldap, krbV +import logging + +from ipapython import ipautil +from ipaserver.install import replication, installutils +from ipaserver import ipaldap +from ipapython import version +from ipalib import api, errors, util +from ipalib.dn import DN + +CACERT = "/etc/ipa/ca.crt" +PORT = 7389 + +# dict of command name and tuples of min/max num of args needed +commands = { + "list":(0, 1, "[master fqdn]", ""), + "connect":(1, 2, " [other master fqdn]", + "must provide the name of the servers to connect"), + "disconnect":(1, 2, " [other master fqdn]", + "must provide the name of the server to disconnect"), + "del":(1, 1, "", + "must provide hostname of master to delete"), + "re-initialize":(0, 0, "", ""), + "force-sync":(0, 0, "", "") +} + +def convert_error(exc): + """ + LDAP exceptions are a dictionary, make them prettier. + """ + if isinstance(exc, ldap.LDAPError): + desc = exc.args[0]['desc'].strip() + info = exc.args[0].get('info', '').strip() + return '%s %s' % (desc, info) + else: + return str(exc) + +class CSReplicationManager(replication.ReplicationManager): + + def __init__(self, realm, hostname, dirman_passwd, port=PORT, starttls=True): + super(CSReplicationManager, self).__init__(realm, hostname, dirman_passwd, port, starttls) + self.suffix = 'o=ipaca' + self.hostnames = [] # set before calling or agreement_dn() will fail + + def agreement_dn(self, hostname, master=None): + """ + Construct a dogtag replication agreement name. This needs to be much + more agressive than the IPA replication agreements because the name + is different on each side. + + hostname is the local hostname, not the remote one, for both sides + + NOTE: The agreement number is hardcoded in dogtag as well + + TODO: configurable instance name + """ + dn = None + cn = None + instance_name = 'pki-ca' + + # if master is not None we know what dn to return: + if master is not None: + if master is True: + name = "master" + else: + name = "clone" + cn="%sAgreement1-%s-%s" % (name, hostname, instance_name) + dn = str(DN("cn=%s, %s" % (cn, self.replica_dn()))) + return (cn, dn) + + for host in self.hostnames: + for master in ["master", "clone"]: + try: + cn="%sAgreement1-%s-%s" % (master, host, instance_name) + dn = "cn=%s, %s" % (cn, self.replica_dn()) + self.conn.getEntry(dn, ldap.SCOPE_BASE) + return (cn, dn) + except errors.NotFound: + dn = None + cn = None + + raise errors.NotFound(reason='No agreement found for %s' % hostname) + + def delete_referral(self, hostname): + esc1_suffix = self.suffix.replace('=', '\\3D').replace(',', '\\2C') + esc2_suffix = self.suffix.replace('=', '%3D').replace(',', '%2C') + dn = 'cn=%s,cn=mapping tree,cn=config' % esc1_suffix + # TODO: should we detect proto/port somehow ? + mod = [(ldap.MOD_DELETE, 'nsslapd-referral', + 'ldap://%s:%s/%s' % (hostname, PORT, esc2_suffix))] + + try: + self.conn.modify_s(dn, mod) + except Exception, e: + logging.debug("Failed to remove referral value: %s" % convert_error(e)) + +def parse_options(): + from optparse import OptionParser + + parser = OptionParser(version=version.VERSION) + parser.add_option("-H", "--host", dest="host", help="starting host") + parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password") + parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False, + help="provide additional information") + parser.add_option("-f", "--force", dest="force", action="store_true", default=False, + help="ignore some types of errors") + parser.add_option("--from", dest="fromhost", help="Host to get data from") + + options, args = parser.parse_args() + + valid_syntax = False + + if len(args): + n = len(args) - 1 + k = commands.keys() + for cmd in k: + if cmd == args[0]: + v = commands[cmd] + err = None + if n < v[0]: + err = v[3] + elif n > v[1]: + err = "too many arguments" + else: + valid_syntax = True + if err: + parser.error("Invalid syntax: %s\nUsage: %s [options] %s" % (err, cmd, v[2])) + + if not valid_syntax: + cmdstr = " | ".join(commands.keys()) + parser.error("must provide a command [%s]" % cmdstr) + + # set log level + if options.verbose: + # if verbose, output events at INFO level if not already + mylogger = logging.getLogger() + if mylogger.getEffectiveLevel() > logging.INFO: + mylogger.setLevel(logging.INFO) + # else user has already configured logging externally lower + return options, args + +def list_replicas(realm, host, replica, dirman_passwd, verbose): + + peers = {} + + try: + # connect to main IPA LDAP server + conn = ipaldap.IPAdmin(host, 636, cacert=CACERT) + conn.do_simple_bind(bindpw=dirman_passwd) + + dn = str(DN('cn=masters,cn=ipa,cn=etc,%s' % util.realm_to_suffix(realm))) + entries = conn.search_s(dn, ldap.SCOPE_ONELEVEL) + + for ent in entries: + try: + cadn = DN(('cn', 'CA'), DN(ent.dn)) + entry = conn.getEntry(str(cadn), ldap.SCOPE_BASE) + peers[ent.cn] = ['master', ''] + except errors.NotFound: + peers[ent.cn] = ['CA not configured', ''] + + except Exception, e: + sys.exit("Failed to get data from '%s': %s" % (host, convert_error(e))) + finally: + conn.unbind_s() + + if not replica: + for k, p in peers.iteritems(): + print '%s: %s' % (k, p[0]) + return + + repl = CSReplicationManager(realm, replica, dirman_passwd, PORT, True) + entries = repl.find_replication_agreements() + + for entry in entries: + print '%s' % entry.nsds5replicahost + + if verbose: + print " last init status: %s" % entry.nsds5replicalastinitstatus + print " last init ended: %s" % str(ipautil.parse_generalized_time(entry.nsds5replicalastinitend)) + print " last update status: %s" % entry.nsds5replicalastupdatestatus + print " last update ended: %s" % str(ipautil.parse_generalized_time(entry.nsds5replicalastupdateend)) + +def del_link(realm, replica1, replica2, dirman_passwd, force=False): + + repl2 = None + + try: + repl1 = CSReplicationManager(realm, replica1, dirman_passwd, PORT, True) + + repl1.hostnames = [replica1, replica2] + type1 = repl1.get_agreement_type(replica2) + + repl_list = repl1.find_ipa_replication_agreements() + if not force and len(repl_list) <= 1: + print "Cannot remove the last replication link of '%s'" % replica1 + print "Please use the 'del' command to remove it from the domain" + sys.exit(1) + + except ldap.NO_SUCH_OBJECT: + sys.exit("'%s' has no replication agreement for '%s'" % (replica1, replica2)) + except errors.NotFound: + sys.exit("'%s' has no replication agreement for '%s'" % (replica1, replica2)) + except ldap.SERVER_DOWN, e: + sys.exit("Unable to connect to %s:%d: %s" % (replica1, PORT, convert_error(e))) + except Exception, e: + sys.exit("Failed to get data from '%s': %s" % (replica1, convert_error(e))) + + try: + repl2 = CSReplicationManager(realm, replica2, dirman_passwd, PORT, True) + repl2.hostnames = [replica1, replica2] + + repl_list = repl1.find_ipa_replication_agreements() + if not force and len(repl_list) <= 1: + print "Cannot remove the last replication link of '%s'" % replica2 + print "Please use the 'del' command to remove it from the domain" + sys.exit(1) + + except ldap.NO_SUCH_OBJECT: + print "'%s' has no replication agreement for '%s'" % (replica2, replica1) + if not force: + sys.exit(1) + except errors.NotFound: + print "'%s' has no replication agreement for '%s'" % (replica2, replica1) + if not force: + return + except Exception, e: + print "Failed to get data from '%s': %s" % (replica2, convert_error(e)) + if not force: + sys.exit(1) + + if repl2: + failed = False + try: + repl2.delete_agreement(replica1) + repl2.delete_referral(replica1) + except Exception, e: + print "Unable to remove agreement on %s: %s" % (replica2, convert_error(e)) + failed = True + + if failed: + if force: + print "Forcing removal on '%s'" % replica1 + else: + sys.exit(1) + + if not repl2 and force: + print "Forcing removal on '%s'" % replica1 + + repl1.delete_agreement(replica2) + repl1.delete_referral(replica2) + +def del_master(realm, hostname, options): + + force_del = False + + delrepl = None + # 1. Connect to the dogtag DS to be removed. + try: + delrepl = CSReplicationManager(realm, hostname, options.dirman_passwd) + except Exception, e: + if not options.force: + print "Unable to delete replica %s: %s" % (hostname, convert_error(e)) + sys.exit(1) + else: + print "Unable to connect to replica %s, forcing removal" % hostname + force_del = True + + # 2. Connect to the local dogtag DS server + try: + thisrepl = CSReplicationManager(realm, options.host, + options.dirman_passwd) + except Exception, e: + sys.exit("Failed to connect to server %s: %s" % (options.host, convert_error(e))) + + # 2. Get list of agreements. + if delrepl is None: + # server not up, just remove it from this server + replica_names = [options.host] + else: + replica_names = delrepl.find_ipa_replication_agreements() + + # 3. Remove each agreement + for r in replica_names: + try: + del_link(realm, r, hostname, options.dirman_passwd, force=True) + except Exception, e: + sys.exit("There were issues removing a connection: %s" % convert_error(e)) + +def add_link(realm, replica1, replica2, dirman_passwd, options): + try: + conn = ipaldap.IPAdmin(replica2, 636, cacert=CACERT) + conn.do_simple_bind(bindpw=dirman_passwd) + + dn = str(DN('cn=CA,cn=%s,cn=masters,cn=ipa,cn=etc,%s' % (replica2, util.realm_to_suffix(realm)))) + conn.search_s(dn, ldap.SCOPE_ONELEVEL) + conn.unbind_s() + except ldap.NO_SUCH_OBJECT: + sys.exit('%s does not have a CA configured.' % replica2) + except ldap.SERVER_DOWN, e: + sys.exit("Unable to connect to %s:636: %s" % (replica2, convert_error(e))) + except Exception, e: + sys.exit("Failed to get data from '%s': %s" % (replica1, convert_error(e))) + + try: + repl1 = CSReplicationManager(realm, replica1, dirman_passwd, PORT, True) + entries = repl1.find_replication_agreements() + for e in entries: + if replica1 in e.dn or replica2 in e.dn: + sys.exit('This replication agreement already exists.') + repl1.hostnames = [replica1, replica2] + + except ldap.NO_SUCH_OBJECT: + sys.exit("Cannot find replica '%s'" % replica1) + except ldap.SERVER_DOWN, e: + sys.exit("Unable to connect to %s:%d %s" % (replica1, PORT, convert_error(e))) + except Exception, e: + sys.exit("Failed to get data from '%s': %s" % (replica1, convert_error(e))) + + repl1.setup_replication(replica2, PORT, 0, "cn=Directory Manager", dirman_passwd, True) + print "Connected '%s' to '%s'" % (replica1, replica2) + +def re_initialize(realm, options): + + if not options.fromhost: + sys.exit("re-initialize requires the option --from ") + + repl = CSReplicationManager(realm, options.fromhost, options.dirman_passwd, + PORT, True) + + thishost = installutils.get_fqdn() + + filter = "(&(nsDS5ReplicaHost=%s)(|(objectclass=nsDSWindowsReplicationAgreement)(objectclass=nsds5ReplicationAgreement)))" % thishost + entry = repl.conn.search_s("cn=config", ldap.SCOPE_SUBTREE, filter) + if len(entry) == 0: + logging.error("Unable to find %s -> %s replication agreement" % (options.fromhost, thishost)) + sys.exit(1) + if len(entry) > 1: + logging.error("Found multiple agreements for %s. Only initializing the first one returned: %s" % (thishost, entry[0].dn)) + + repl.initialize_replication(entry[0].dn, repl.conn) + repl.wait_for_repl_init(repl.conn, entry[0].dn) + +def force_sync(realm, thishost, fromhost, dirman_passwd): + + repl = CSReplicationManager(realm, fromhost, dirman_passwd, PORT, True) + try: + repl.force_sync(repl.conn, thishost) + except Exception, e: + sys.exit(convert_error(e)) + +def main(): + options, args = parse_options() + + # Just initialize the environment. This is so the installer can have + # access to the plugin environment + api_env = {} + api_env['in_server'] = True + + if os.getegid() != 0: + api_env['log'] = None # turn off logging for non-root + + api.bootstrap(**api_env) + api.finalize() + + dirman_passwd = None + realm = krbV.default_context().default_realm + + if options.host: + host = options.host + else: + host = installutils.get_fqdn() + + options.host = host + + if options.dirman_passwd: + dirman_passwd = options.dirman_passwd + else: + dirman_passwd = getpass.getpass("Directory Manager password: ") + + options.dirman_passwd = dirman_passwd + + if args[0] == "list": + replica = None + if len(args) == 2: + replica = args[1] + list_replicas(realm, host, replica, dirman_passwd, options.verbose) + elif args[0] == "del": + del_master(realm, args[1], options) + elif args[0] == "re-initialize": + re_initialize(realm, options) + elif args[0] == "force-sync": + if not options.fromhost: + sys.exit("force-sync requires the option --from ") + force_sync(realm, host, options.fromhost, options.dirman_passwd) + elif args[0] == "connect": + if len(args) == 3: + replica1 = args[1] + replica2 = args[2] + elif len(args) == 2: + replica1 = host + replica2 = args[1] + add_link(realm, replica1, replica2, dirman_passwd, options) + elif args[0] == "disconnect": + if len(args) == 3: + replica1 = args[1] + replica2 = args[2] + elif len(args) == 2: + replica1 = host + replica2 = args[1] + del_link(realm, replica1, replica2, dirman_passwd) + +try: + main() +except KeyboardInterrupt: + sys.exit(1) +except SystemExit, e: + sys.exit(e) +except ldap.INVALID_CREDENTIALS: + sys.exit("Invalid password") +except ldap.INSUFFICIENT_ACCESS: + sys.exit("Insufficient access") +except ldap.LOCAL_ERROR, e: + sys.exit(convert_error(e)) +except ldap.SERVER_DOWN, e: + sys.exit("%s" % convert_error(e)) +except Exception, e: + sys.exit("unexpected error: %s" % convert_error(e)) diff --git a/install/tools/man/Makefile.am b/install/tools/man/Makefile.am index 63a598ac2..973e913ca 100644 --- a/install/tools/man/Makefile.am +++ b/install/tools/man/Makefile.am @@ -8,6 +8,7 @@ man1_MANS = \ ipa-replica-conncheck.1 \ ipa-replica-install.1 \ ipa-replica-manage.1 \ + ipa-csreplica-manage.1 \ ipa-replica-prepare.1 \ ipa-server-certinstall.1 \ ipa-server-install.1 \ diff --git a/install/tools/man/ipa-csreplica-manage.1 b/install/tools/man/ipa-csreplica-manage.1 new file mode 100644 index 000000000..6c9361ebe --- /dev/null +++ b/install/tools/man/ipa-csreplica-manage.1 @@ -0,0 +1,93 @@ +.\" A man page for ipa-csreplica-manage +.\" Copyright (C) 2011 Red Hat, Inc. +.\" +.\" 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, either version 3 of the License, or +.\" (at your option) any later version. +.\" +.\" 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, see . +.\" +.\" Author: Rob Crittenden +.\" +.TH "ipa-replica-manage" "1" "Jul 14 2011" "freeipa" "" +.SH "NAME" +ipa\-replica\-manage \- Manage an IPA CS replica +.SH "SYNOPSIS" +ipa\-replica\-manage [\fIOPTION\fR]... [connect|disconnect|del|list|re\-initialize|force\-sync] +.SH "DESCRIPTION" +Manages the CA replication agreements of an IPA server. +.TP +\fBconnect\fR [SERVER_A] +\- Adds a new replication agreement between SERVER_A/localhost and SERVER_B +.TP +\fBdisconnect\fR [SERVER_A] +\- Removes a replication agreement between SERVER_A/localhost and SERVER_B +.TP +\fBdel\fR +\- Removes all replication agreements and data about SERVER +.TP +\fBlist\fR [SERVER] +\- Lists all the servers or the list of agreements of SERVER +.TP +\fBre\-initialize\fR +\- Forces a full re\-initialization of the IPA CA server retrieving data from the server specified with the \-\-from option +.TP +\fBforce\-sync\fR +\- Immediately flush any data to be replicated from a server specified with the \-\-from option +.TP +The connect and disconnect options are used to manage the replication topology. When a replica is created it is only connected with the master that created it. The connect option may be used to connect it to other existing replicas. +.TP +The disconnect option cannot be used to remove the last link of a replica. To remove a replica from the topology use the del option. +.TP +If a replica is deleted and then re\-added within a short time-frame then the 389\-ds instance on the master that created it should be restarted before re\-installing the replica. The master will have the old service principals cached which will cause replication to fail. +.SH "OPTIONS" +.TP +\fB\-H\fR \fIHOST\fR, \fB\-\-host\fR=\fIHOST\fR +The IPA server to manage. +The default is the machine on which the command is run +Not honoured by the re\-initialize command. +.TP +\fB\-p\fR \fIDM_PASSWORD\fR, \fB\-\-password\fR=\fIDM_PASSWORD\fR +The Directory Manager password to use for authentication +.TP +\fB\-v\fR, \fB\-\-verbose\fR +Provide additional information +.TP +\fB\-f\fR, \fB\-\-force\fR +Ignore some types of errors +.TP +\fB\-\-from\fR=\fISERVER\fR +The server to pull the data from, used by the re\-initialize and force\-sync commands. +.SH "EXAMPLES" +.TP +List a server's replication agreements. + # ipa\-csreplica\-manage list srv1.example.com + srv2.example.com + srv3.example.com +.TP +Re\-initialize a replica: + # ipa\-csreplica\-manage re\-initialize \-\-from srv2.example.com + +This will re\-initialize the data on the server where you execute the command, retrieving the data from the srv2.example.com replica +.TP +Add a new replication agreement: + # ipa\-csreplica\-manage connect srv2.example.com srv4.example.com +.TP +Remove an existing replication agreement: + # ipa\-csreplica\-manage disconnect srv1.example.com srv3.example.com +.TP +Completely remove a replica: + # ipa\-csreplica\-manage del srv4.example.com +.TP +Using connect/disconnect you can manage the replication topology. +.SH "EXIT STATUS" +0 if the command was successful +.TP +1 if an error occurred diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py index 9033b7bfd..99b021590 100644 --- a/ipaserver/install/dsinstance.py +++ b/ipaserver/install/dsinstance.py @@ -305,8 +305,8 @@ class DsInstance(service.Service): self.fqdn, self.dm_password) repl.setup_replication(self.master_fqdn, - "cn=Directory Manager", - self.dm_password) + r_binddn="cn=Directory Manager", + r_bindpw=self.dm_password) def __enable(self): self.backup_state("enabled", self.is_enabled()) diff --git a/ipaserver/install/replication.py b/ipaserver/install/replication.py index 22d4e1ae5..da8e749e4 100644 --- a/ipaserver/install/replication.py +++ b/ipaserver/install/replication.py @@ -29,6 +29,7 @@ from ldap import modlist from ipalib import util from ipalib import errors from ipapython import ipautil +from ipalib.dn import DN DIRMAN_CN = "cn=directory manager" CACERT = "/etc/ipa/ca.crt" @@ -38,6 +39,7 @@ WIN_USER_CONTAINER = "cn=Users" IPA_USER_CONTAINER = "cn=users,cn=accounts" PORT = 636 TIMEOUT = 120 +REPL_MAN_DN = "cn=replication manager,cn=config" IPA_REPLICA = 1 WINSYNC = 2 @@ -108,19 +110,26 @@ def enable_replication_version_checking(hostname, realm, dirman_passwd): else: conn.unbind() -class ReplicationManager: +class ReplicationManager(object): """Manage replication agreements between DS servers, and sync agreements with Windows servers""" - def __init__(self, realm, hostname, dirman_passwd): + def __init__(self, realm, hostname, dirman_passwd, port=PORT, starttls=False): self.hostname = hostname + self.port = port self.dirman_passwd = dirman_passwd self.realm = realm + self.starttls = starttls tmp = util.realm_to_suffix(realm) self.suffix = ipaldap.IPAdmin.normalizeDN(tmp) # If we are passed a password we'll use it as the DM password # otherwise we'll do a GSSAPI bind. - self.conn = ipaldap.IPAdmin(hostname, port=PORT, cacert=CACERT) + if starttls: + self.conn = ipaldap.IPAdmin(hostname, port=port) + ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, CACERT) + self.conn.start_tls_s() + else: + self.conn = ipaldap.IPAdmin(hostname, port=port, cacert=CACERT) if dirman_passwd: self.conn.do_simple_bind(bindpw=dirman_passwd) else: @@ -130,7 +139,7 @@ class ReplicationManager: # 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_dn = REPL_MAN_DN self.repl_man_cn = "replication manager" def _get_replica_id(self, conn, master_conn): @@ -152,7 +161,7 @@ class ReplicationManager: # 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 + dn = str(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'): @@ -235,7 +244,7 @@ class ReplicationManager: conn.modify_s(dn, [(ldap.MOD_REPLACE, "userpassword", pw)]) pass - def delete_replication_manager(self, conn, dn="cn=replication manager,cn=config"): + def delete_replication_manager(self, conn, dn=REPL_MAN_DN): try: conn.delete_s(dn) except ldap.NO_SUCH_OBJECT: @@ -248,13 +257,21 @@ class ReplicationManager: return "2" def replica_dn(self): - return 'cn=replica, cn="%s", cn=mapping tree, cn=config' % self.suffix + return str(DN('cn=replica, cn="%s", cn=mapping tree, cn=config' % self.suffix)) def replica_config(self, conn, replica_id, replica_binddn): dn = self.replica_dn() try: - conn.getEntry(dn, ldap.SCOPE_BASE) + entry = conn.getEntry(dn, ldap.SCOPE_BASE) + managers = entry.getValues('nsDS5ReplicaBindDN') + for m in managers: + if DN(replica_binddn) == DN(m): + return + # Add the new replication manager + mod = [(ldap.MOD_ADD, 'nsDS5ReplicaBindDN', replica_binddn)] + conn.modify_s(dn, mod) + # replication is already configured return except errors.NotFound: @@ -409,24 +426,34 @@ class ReplicationManager: entry.setValues("nsds7NewWinGroupSyncEnabled", 'false') entry.setValues("nsds7WindowsDomain", windomain) - def agreement_dn(self, hostname): + def agreement_dn(self, hostname, master=None): + """ + IPA agreement use the same dn on both sides, dogtag does not. + master is not used for IPA agreements but for dogtag it will + tell which side we want. + """ cn = "meTo%s" % (hostname) dn = "cn=%s, %s" % (cn, self.replica_dn()) return (cn, dn) - def setup_agreement(self, a_conn, b_hostname, + def setup_agreement(self, a_conn, b_hostname, port=389, repl_man_dn=None, repl_man_passwd=None, - iswinsync=False, win_subtree=None, isgssapi=False): - cn, dn = self.agreement_dn(b_hostname) + iswinsync=False, win_subtree=None, isgssapi=False, + master=None): + """ + master is used to determine which side of the agreement we are + creating. This is only needed for dogtag replication agreements + which use a different name on each side. If master is None then + isn't a dogtag replication agreement. + """ + cn, dn = self.agreement_dn(b_hostname, master=master) try: a_conn.getEntry(dn, ldap.SCOPE_BASE) return except errors.NotFound: pass - port = 389 - # List of attributes that need to be excluded from replication. excludes = ('memberof', 'entryusn', 'krblastsuccessfulauth', @@ -440,9 +467,10 @@ class ReplicationManager: entry.setValues('nsds5replicaport', str(port)) entry.setValues('nsds5replicatimeout', str(TIMEOUT)) entry.setValues('nsds5replicaroot', self.suffix) - entry.setValues('nsds5replicaupdateschedule', '0000-2359 0123456') - entry.setValues('nsDS5ReplicatedAttributeList', - '(objectclass=*) $ EXCLUDE %s' % " ".join(excludes)) + if master is None: + entry.setValues('nsds5replicaupdateschedule', '0000-2359 0123456') + entry.setValues('nsDS5ReplicatedAttributeList', + '(objectclass=*) $ EXCLUDE %s' % " ".join(excludes)) entry.setValues('description', "me to %s" % b_hostname) if isgssapi: entry.setValues('nsds5replicatransportinfo', 'LDAP') @@ -623,11 +651,11 @@ class ReplicationManager: haserror = 1 return haserror - def start_replication(self, conn, hostname=None): + def start_replication(self, conn, hostname=None, master=None): print "Starting replication, please wait until this has completed." if hostname == None: hostname = self.conn.host - cn, dn = self.agreement_dn(hostname) + cn, dn = self.agreement_dn(hostname, master) mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')] conn.modify_s(dn, mod) @@ -640,10 +668,16 @@ class ReplicationManager: self.replica_config(conn, replica_id, repldn) self.setup_changelog(conn) - def setup_replication(self, r_hostname, r_binddn=None, r_bindpw=None): + def setup_replication(self, r_hostname, r_port=389, r_sslport=636, r_binddn=None, r_bindpw=None, starttls=False): # note - there appears to be a bug in python-ldap - it does not # allow connections using two different CA certs - r_conn = ipaldap.IPAdmin(r_hostname, port=PORT, cacert=CACERT) + if starttls: + r_conn = ipaldap.IPAdmin(r_hostname, port=r_port) + ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, CACERT) + r_conn.start_tls_s() + else: + r_conn = ipaldap.IPAdmin(r_hostname, port=r_sslport, cacert=CACERT) + if r_bindpw: r_conn.do_simple_bind(binddn=r_binddn, bindpw=r_bindpw) else: @@ -659,15 +693,17 @@ class ReplicationManager: self.basic_replication_setup(r_conn, r_id, self.repl_man_dn, self.repl_man_passwd) - self.setup_agreement(r_conn, self.conn.host, + self.setup_agreement(r_conn, self.conn.host, port=r_port, repl_man_dn=self.repl_man_dn, - repl_man_passwd=self.repl_man_passwd) - self.setup_agreement(self.conn, r_hostname, + repl_man_passwd=self.repl_man_passwd, + master=True) + self.setup_agreement(self.conn, r_hostname, port=r_port, repl_man_dn=self.repl_man_dn, - repl_man_passwd=self.repl_man_passwd) + repl_man_passwd=self.repl_man_passwd, + master=False) #Finally start replication - ret = self.start_replication(r_conn) + ret = self.start_replication(r_conn, master=True) if ret != 0: raise RuntimeError("Failed to start replication") @@ -717,7 +753,7 @@ class ReplicationManager: logging.info("Agreement is ready, starting replication . . .") # Add winsync replica to the public DIT - dn = 'cn=%s,cn=replicas,cn=ipa,cn=etc,%s' % (ad_dc_name, self.suffix) + dn = str(DN('cn=%s,cn=replicas,cn=ipa,cn=etc,%s' % (ad_dc_name, self.suffix))) entry = ipaldap.Entry(dn) entry.setValues("objectclass", ["nsContainer", "ipaConfigObject"]) entry.setValues("cn", ad_dc_name) @@ -802,6 +838,8 @@ class ReplicationManager: dn = entry[0].dn schedule = entry[0].nsds5replicaupdateschedule + if schedule is None: + schedule = '0000-2359 0123456' # 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 -- cgit