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

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