Martin Kosek wrote:
On Fri, 2011-07-15 at 14:43 +0200, Jan Cholasta wrote:
On 15.7.2011 05:42, Rob Crittenden wrote:
Add a separate tool for now to do dogtag replication agreement
management. The syntax is the same for IPA agreements with the exception
that the DM password is always required and it isn't possible to
delegate the management of this.

ticket https://fedorahosted.org/freeipa/ticket/1250

rob


NACK

'ipa-csreplica-manage list server' doesn't list the peers of the
specified server, but the peers of localhost.

Connecting already connected pair of replicas duplicates the replication
information ('ipa-csreplica-manage list server' shows the same hostname
twice).

There is trailing whitespace on line 87 of the patch.

BTW I don't understand why is it possible (or necessary?) to be able to
have CS replication topology that is different from the main IPA
replication topology (ipa-csreplica-manage allows you to do that). Is
there a reason for this?

Honza


And some issues from me:

1) Unhelpful error message when force-syncing from a master without a
replication agreement:

# ipa-csreplica-manage force-sync --from=HOST
Directory Manager password:
ipa: ERROR: Unable to find replication agreement for 
vm-060.idm.lab.bos.redhat.com
unexpected error: Unable to proceed

2) Minor stuff in man page:

Unindented Exit statuses:
EXIT STATUS
        0 if the command was successful
         1 if an error occurred

Missing dot: The default is the machine on which the command is run  Not
               honoured by the re-initialize command.


Otherwise it looks good.

Martin


This should address all the issues raised.

The reason for different topology has several reasons:

1. A given IPA server may not have a CA installed
2. Some aspects of ipa-replica-manage can be delegated. We can't delegate CS replica management because it is in a different directory server. We don't have users stored there so can't map the GSSAPI credentials. So only Directory Manager can operate on it for now.
3. Flexibility. You may want way more connections for users than for the CA.

rob
>From ab416ad4e7bc6e73fda59f86551d2d54a1534511 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcrit...@redhat.com>
Date: Thu, 14 Jul 2011 23:35:01 -0400
Subject: [PATCH] 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         |   77 ++++--
 7 files changed, 610 insertions(+), 25 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 e4aa0f6265d37dd250bfdb0d9ea80023744223aa..fc539ca7b5c74995de476892835832317672ae6b 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 <ayo...@redhat.com> - 2.0.90-5
+* Thu Jul 14 2011 Rob Crittenden <rcrit...@redhat.com> - 2.0.90-6
+- Add ipa-csreplica-manage tool.
+
+* Wed Jul  6 2011 Adam Young <ayo...@redhat.com> - 2.0.90-5
 - Add HTML file describing issues with HBAC deny rules
 
 * Fri Jun 17 2011 Rob Crittenden <rcrit...@redhat.com> - 2.0.90-4
diff --git a/install/tools/Makefile.am b/install/tools/Makefile.am
index c6ecd92876adb5ba5dd5eef041502c27e56bb811..fc615ec04f324c2d9c98dc8cf674938e1064bec6 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 0000000000000000000000000000000000000000..b61655ba1d570f08bfc683b2591519a3390b05a7
--- /dev/null
+++ b/install/tools/ipa-csreplica-manage
@@ -0,0 +1,452 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcrit...@redhat.com>
+#
+# Based on ipa-replica-manage by Karl MacMillan <kmacmil...@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 = "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 = '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 = '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 63a598ac2a1297c03a308a5efc9388329167a5b5..973e913ca0f6aa3561a2fd1bb763c3d8162e1269 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 0000000000000000000000000000000000000000..6c9361ebea0bde51eb3b76e10657381c85bcc2c8
--- /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 <rcrit...@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 9033b7bfdb260ad54982a65f642c0901d12ac491..99b021590bdd233a1e0c72c91a3e78a34fd2cd65 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 22d4e1ae5d1360d7fcc2ab7174feb2185d24ba5b..a13944b945ac04343c59d938f7816c783cd00553 100644
--- a/ipaserver/install/replication.py
+++ b/ipaserver/install/replication.py
@@ -108,19 +108,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:
@@ -254,7 +261,12 @@ class ReplicationManager:
         dn = self.replica_dn()
 
         try:
-            conn.getEntry(dn, ldap.SCOPE_BASE)
+            entry = conn.getEntry(dn, ldap.SCOPE_BASE)
+            managers = entry.getValues('nsDS5ReplicaBindDN')
+            if replica_binddn not in managers:
+                mod = [(ldap.MOD_ADD, 'nsDS5ReplicaBindDN', replica_binddn)]
+                conn.modify_s(dn, mod)
+
             # replication is already configured
             return
         except errors.NotFound:
@@ -409,24 +421,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 +462,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 +646,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 +663,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 +688,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")
 
@@ -802,6 +833,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
-- 
1.7.4

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to