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
This commit is contained in:
Rob Crittenden 2011-07-14 23:35:01 -04:00
parent 2f650b60a4
commit 3fdca99c48
7 changed files with 620 additions and 30 deletions

View File

@ -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

View File

@ -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 \

View File

@ -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))

View File

@ -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 \

View File

@ -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

View File

@ -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())

View File

@ -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