summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--freeipa.spec.in7
-rw-r--r--install/tools/Makefile.am1
-rwxr-xr-xinstall/tools/ipa-csreplica-manage452
-rw-r--r--install/tools/man/Makefile.am1
-rw-r--r--install/tools/man/ipa-csreplica-manage.193
-rw-r--r--ipaserver/install/dsinstance.py4
-rw-r--r--ipaserver/install/replication.py92
7 files changed, 620 insertions, 30 deletions
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 <ayoung@redhat.com> - 2.0.90-5
+* Thu Jul 14 2011 Rob Crittenden <rcritten@redhat.com> - 2.0.90-6
+- Add ipa-csreplica-manage tool.
+
+* Wed Jul 6 2011 Adam Young <ayoung@redhat.com> - 2.0.90-5
- Add HTML file describing issues with HBAC deny rules
* Fri Jun 17 2011 Rob Crittenden <rcritten@redhat.com> - 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 <rcritten@redhat.com>
+#
+# Based on ipa-replica-manage by Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+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, "<master fqdn> [other master fqdn]",
+ "must provide the name of the servers to connect"),
+ "disconnect":(1, 2, "<master fqdn> [other master fqdn]",
+ "must provide the name of the server to disconnect"),
+ "del":(1, 1, "<master fqdn>",
+ "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 <host name>")
+
+ 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 <host name>")
+ 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 <http://www.gnu.org/licenses/>.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.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] <SERVER_B>
+\- Adds a new replication agreement between SERVER_A/localhost and SERVER_B
+.TP
+\fBdisconnect\fR [SERVER_A] <SERVER_B>
+\- Removes a replication agreement between SERVER_A/localhost and SERVER_B
+.TP
+\fBdel\fR <SERVER>
+\- 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