This is an automated email from the git hooks/post-receive script.
firstyear pushed a commit to branch master
in repository 389-ds-base.
commit c412ea6ba7880e8a26341998d69e0965e463220b
Author: Thierry bordaz (tbordaz) <tbordaz(a)redhat.com>
Date: Mon Jan 6 17:26:05 2014 +0100
Ticket 47600 : Replica/Agreement/Changelog not conform to the design
Bug Description:
Currently all replication functions are in brooker/__ini__.
To conform the design, we need to split these functions into various classes
Replica/Agreement/Changelog (and files) and to remove them from brooker/__init__.
Also to change the interfaces (list/create/delete/set/get..).
Fix Description:
Create Agreement/Replica/Changelog classes with
(list/create/delete/set-getProperties...)
Add unit tests for Replica and RA
Fix some issues found with unit tests
https://fedorahosted.org/389/ticket/47600
Reviewed by: Rich Megginson (thanks Rich !!)
Platforms tested: F17
Flag Day: no
Doc impact: no
---
src/lib389/lib389/__init__.py | 81 +---
src/lib389/lib389/_constants.py | 31 +-
src/lib389/lib389/_entry.py | 34 ++
src/lib389/lib389/agreement.py | 645 +++++++++++++++++++++++++
src/lib389/lib389/brooker.py | 798 -------------------------------
src/lib389/lib389/changelog.py | 96 ++++
src/lib389/lib389/properties.py | 156 ++++++-
src/lib389/lib389/replica.py | 553 ++++++++++++++++++++++
src/lib389/lib389/suffix.py | 8 +
src/lib389/tests/agreement_test.py | 321 +++++++++++++
src/lib389/tests/replica_test.py | 935 ++++++++++++++++++++++++-------------
11 files changed, 2441 insertions(+), 1217 deletions(-)
diff --git a/src/lib389/lib389/__init__.py b/src/lib389/lib389/__init__.py
index 60ac2f2..6e2b357 100644
--- a/src/lib389/lib389/__init__.py
+++ b/src/lib389/lib389/__init__.py
@@ -45,6 +45,8 @@ from ldap.cidict import cidict
from ldap import LDAPError
# file in this package
+
+
from lib389._constants import *
from lib389._entry import Entry
from lib389._replication import CSN, RUV
@@ -297,16 +299,18 @@ class DirSrv(SimpleLDAPObject):
def __add_brookers__(self):
from lib389.brooker import (
- Agreement,
- Replica,
Config,
Index)
from lib389.mappingTree import MappingTree
- from lib389.backend import Backend
- from lib389.suffix import Suffix
+ from lib389.backend import Backend
+ from lib389.suffix import Suffix
+ from lib389.replica import Replica
+ from lib389.changelog import Changelog
+ from lib389.agreement import Agreement
self.agreement = Agreement(self)
self.replica = Replica(self)
+ self.changelog = Changelog(self)
self.backend = Backend(self)
self.config = Config(self)
self.index = Index(self)
@@ -1878,74 +1882,7 @@ class DirSrv(SimpleLDAPObject):
def startReplication(self, agmtdn):
return self.replica.start_and_wait(agmtdn)
- def enableReplication(self, suffix=None, role=None, replicaId=CONSUMER_REPLICAID,
binddn=None):
- if not suffix:
- log.fatal("enableReplication: suffix not specified")
- raise ValueError("suffix missing")
-
- if not role:
- log.fatal("enableReplication: replica role not specify
(REPLICAROLE_*)")
- raise ValueError("role missing")
-
- #
- # Check the validity of the parameters
- #
-
- # First role and replicaID
- if role != REPLICAROLE_MASTER and role != REPLICAROLE_HUB and role !=
REPLICAROLE_CONSUMER:
- log.fatal("enableReplication: replica role invalid (%s) " % role)
- raise ValueError("invalid role: %s" % role)
-
- # master
- replica_type = REPLICA_RDWR_TYPE
- else:
- # hub or consumer
- replica_type = REPLICA_RDONLY_TYPE
-
- if role == REPLICAROLE_MASTER:
- # check the replicaId [1..CONSUMER_REPLICAID[
- if not decimal.Decimal(replicaId) or (replicaId <= 0) or (replicaId
>=CONSUMER_REPLICAID):
- log.fatal("enableReplication: invalid replicaId (%s) for a RW
replica" % replicaId)
- raise ValueError("invalid replicaId %d (expected
[1..CONSUMER_REPLICAID[" % replicaId)
- elif replicaId != CONSUMER_REPLICAID:
- # check the replicaId is CONSUMER_REPLICAID
- log.fatal("enableReplication: invalid replicaId (%s) for a Read replica
(expected %d)" % (replicaId, CONSUMER_REPLICAID))
- raise ValueError("invalid replicaId: %d for HUB/CONSUMER replicaId is
CONSUMER_REPLICAID" % replicaId)
-
- # Now check we have a suffix
- entries_backend = self.backend.list(suffix=suffix)
- if not entries_backend:
- log.fatal("enableReplication: enable to retrieve the backend for
%s" % suffix)
- raise ValueError("no backend for suffix %s" % suffix)
-
- ent = entries_backend[0]
- if normalizeDN(suffix) != normalizeDN(ent.getValue('nsslapd-suffix')):
- log.warning("enableReplication: suffix (%s) and backend suffix (%s)
differs" % (suffix, entries_backend[0].nsslapd-suffix))
- pass
-
- # Now prepare the bindDN property
- if not binddn:
- binddn = defaultProperties.get(REPLICATION_BIND_DN, None)
- if not binddn:
- # weird, internal error we do not retrieve the default replication bind
DN
- # this replica will not be updatable through replication until the
binddn
- # property will be set
- log.warning("enableReplication: binddn not provided and default
value unavailable")
- pass
-
- # Now do the effectif job
- # First add the changelog if master/hub
- if (role == REPLICAROLE_MASTER) or (role == REPLICAROLE_HUB):
- self.replica.changelog()
-
- # Second create the default replica manager entry if it does not exist
- # it should not be called from here but for the moment I am unsure when to create
it elsewhere
- self.replica.create_repl_manager()
-
- # then enable replication
- ret = self.replica.add(suffix=suffix, binddn=binddn, role=role, rid=replicaId)
-
- return ret
+
def replicaSetupAll(self, repArgs):
"""setup everything needed to enable replication for a given
suffix.
diff --git a/src/lib389/lib389/_constants.py b/src/lib389/lib389/_constants.py
index 35fbcf3..128e30f 100644
--- a/src/lib389/lib389/_constants.py
+++ b/src/lib389/lib389/_constants.py
@@ -1,7 +1,7 @@
# replicatype @see
https://access.redhat.com/knowledge/docs/en-US/Red_Hat_Directory_Server/8...
# 2 for consumers and hubs (read-only replicas)
# 3 for both single and multi-master suppliers (read-write replicas)
-# TODO: let's find a way to be consistent - eg. using bitwise operator
+
(MASTER_TYPE,
HUB_TYPE,
LEAF_TYPE) = range(3)
@@ -14,16 +14,29 @@ CONSUMER_REPLICAID = 65535
REPLICA_RDONLY_TYPE = 2 # CONSUMER and HUB
REPLICA_WRONLY_TYPE = 1 # SINGLE and MULTI MASTER
-REPLICA_RDWR_TYPE = REPLICA_RDONLY_TYPE | REPLICA_WRONLY_TYPE
+REPLICA_RDWR_TYPE = REPLICA_RDONLY_TYPE | REPLICA_WRONLY_TYPE
-REPLICATION_BIND_DN = 'replication_bind_dn'
-REPLICATION_BIND_PW = 'replication_bind_pw'
+REPLICA_RUV_UUID = "ffffffff-ffffffff-ffffffff-ffffffff"
+REPLICA_OC_TOMBSTONE= "nsTombstone"
+REPLICATION_BIND_DN = 'replication_bind_dn'
+REPLICATION_BIND_PW = 'replication_bind_pw'
REPLICATION_BIND_METHOD = 'replication_bind_method'
+REPLICATION_TRANSPORT = 'replication_transport'
+REPLICATION_TIMEOUT = 'replication_timeout'
+
+TRANS_STARTTLS = "starttls"
+TRANS_SECURE = "secure"
+TRANS_NORMAL = "normal"
+REPL_TRANS_VALUE = {TRANS_STARTTLS: 'TLS',
+ TRANS_SECURE: 'SSL',
+ TRANS_NORMAL: 'LDAP'}
defaultProperties = {
- REPLICATION_BIND_DN: "cn=replrepl,cn=config",
- REPLICATION_BIND_PW: "password",
- REPLICATION_BIND_METHOD: "simple"
+ REPLICATION_BIND_DN: "cn=replrepl,cn=config",
+ REPLICATION_BIND_PW: "password",
+ REPLICATION_BIND_METHOD: "simple",
+ REPLICATION_TRANSPORT: REPL_TRANS_VALUE[TRANS_NORMAL],
+ REPLICATION_TIMEOUT: str(120)
}
@@ -51,6 +64,8 @@ DEFAULT_BENAME = 'userRoot' # warning it is case
sensitive
DEFAULT_BACKUPDIR = '/tmp'
DEFAULT_INST_HEAD = 'slapd-'
DEFAULT_ENV_HEAD = 'dirsrv-'
+DEFAULT_CHANGELOG_NAME = "changelog5"
+DEFAULT_CHANGELOG_DB = 'changelogdb'
PW_DM = "password"
@@ -76,6 +91,8 @@ DN_CHANGELOG = "cn=changelog5,%s" % DN_CONFIG
DN_LDBM = "cn=ldbm database,%s" % DN_PLUGIN
DN_CHAIN = "cn=chaining database,%s" % DN_PLUGIN
+RDN_REPLICA = "cn=replica"
+
#
diff --git a/src/lib389/lib389/_entry.py b/src/lib389/lib389/_entry.py
index 6e3dcd0..f993a36 100644
--- a/src/lib389/lib389/_entry.py
+++ b/src/lib389/lib389/_entry.py
@@ -4,6 +4,9 @@ from ldap.cidict import cidict
import cStringIO
import logging
+import ldap
+from lib389 import *
+from lib389.properties import *
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger(__name__)
@@ -176,3 +179,34 @@ class Entry(object):
ldif.LDIFWriter(
sio, Entry.base64_attrs, 1000).unparse(self.dn, newdata)
return sio.getvalue()
+
+ def create(self, type=ENTRY_TYPE_PERSON, entry_dn=None, properties=None):
+ """ Return - eventually creating - a person entry with the given
dn and pwd.
+
+ binddn can be a lib389.Entry
+ """
+ if not entry_dn:
+ raise ValueError("entry_dn is mandatory")
+
+ if not type:
+ raise ValueError("type is mandatory")
+
+ ent = Entry(entry_dn)
+ ent.setValues(ENTRY_OBJECTCLASS, ENTRY_TYPE_TO_OBJECTCLASS[type])
+ ent.setValues(ENTRY_USERPASSWORD, properties.get(ENTRY_USERPASSWORD,
""))
+ ent.setValues(ENTRY_SN, properties.get(ENTRY_SN, "bind dn pseudo
user"))
+ ent.setValues(ENTRY_CN, properties.get(ENTRY_CN, "bind dn pseudo
user"))
+ if type == ENTRY_TYPE_INETPERSON:
+ ent.setValues(ENTRY_UID, properties.get(ENTRY_UID, "bind dn pseudo
user"))
+
+ try:
+ self.add_s(ent)
+ except ldap.ALREADY_EXISTS:
+ log.warn("Entry %s already exists" % entry_dn)
+
+ try:
+ entry = self._test_entry(entry_dn, ldap.SCOPE_BASE)
+ return entry
+ except MissingEntryError:
+ log.exception("This entry should exist!")
+ raise
diff --git a/src/lib389/lib389/agreement.py b/src/lib389/lib389/agreement.py
new file mode 100644
index 0000000..561e499
--- /dev/null
+++ b/src/lib389/lib389/agreement.py
@@ -0,0 +1,645 @@
+'''
+Created on Dec 5, 2013
+
+@author: tbordaz
+'''
+
+import ldap
+import re
+
+from lib389._constants import *
+from lib389._entry import FormatDict
+from lib389.utils import normalizeDN
+from lib389 import Entry, DirSrv, NoSuchEntryError, InvalidArgumentError
+from lib389.properties import *
+
+class Agreement(object):
+ ALWAYS = '0000-2359 0123456'
+ NEVER = '2358-2359 0'
+
+ proxied_methods = 'search_s getEntry'.split()
+
+ def __init__(self, conn):
+ """@param conn - a DirSrv instance"""
+ self.conn = conn
+ self.log = conn.log
+
+ def __getattr__(self, name):
+ if name in Agreement.proxied_methods:
+ return DirSrv.__getattr__(self.conn, name)
+
+
+
+ def status(self, agreement_dn):
+ """Return a formatted string with the replica status. Looking
like:
+ Status for meTo_localhost.localdomain:50389 agmt localhost.localdomain:50389
+ Update in progress: TRUE
+ Last Update Start: 20131121132756Z
+ Last Update End: 0
+ Num. Changes Sent: 1:10/0
+ Num. changes Skipped: None
+ Last update Status: 0 Replica acquired successfully: Incremental update
started
+ Init in progress: None
+ Last Init Start: 0
+ Last Init End: 0
+ Last Init Status: None
+ Reap Active: 0
+ @param agreement_dn - DN of the replication agreement
+
+ @returns string containing the status of the replica agreement
+
+ @raise NoSuchEntryError - if agreement_dn is an unknown entry
+ """
+
+ attrlist = ['cn', 'nsds5BeginReplicaRefresh',
'nsds5replicaUpdateInProgress',
+ 'nsds5ReplicaLastInitStatus',
'nsds5ReplicaLastInitStart',
+ 'nsds5ReplicaLastInitEnd', 'nsds5replicaReapActive',
+ 'nsds5replicaLastUpdateStart',
'nsds5replicaLastUpdateEnd',
+ 'nsds5replicaChangesSentSinceStartup',
'nsds5replicaLastUpdateStatus',
+ 'nsds5replicaChangesSkippedSinceStartup',
'nsds5ReplicaHost',
+ 'nsds5ReplicaPort']
+ try:
+ ent = self.conn.getEntry(
+ agreement_dn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist)
+ except NoSuchEntryError:
+ raise NoSuchEntryError(
+ "Error reading status from agreement", agreement_dn)
+ else:
+ retstr = (
+ "Status for %(cn)s agmt
%(nsDS5ReplicaHost)s:%(nsDS5ReplicaPort)s" "\n"
+ "Update in progress: %(nsds5replicaUpdateInProgress)s"
"\n"
+ "Last Update Start: %(nsds5replicaLastUpdateStart)s"
"\n"
+ "Last Update End: %(nsds5replicaLastUpdateEnd)s"
"\n"
+ "Num. Changes Sent: %(nsds5replicaChangesSentSinceStartup)s"
"\n"
+ "Num. changes Skipped:
%(nsds5replicaChangesSkippedSinceStartup)s" "\n"
+ "Last update Status: %(nsds5replicaLastUpdateStatus)s"
"\n"
+ "Init in progress: %(nsds5BeginReplicaRefresh)s"
"\n"
+ "Last Init Start: %(nsds5ReplicaLastInitStart)s"
"\n"
+ "Last Init End: %(nsds5ReplicaLastInitEnd)s" "\n"
+ "Last Init Status: %(nsds5ReplicaLastInitStatus)s"
"\n"
+ "Reap Active: %(nsds5ReplicaReapActive)s" "\n"
+ )
+ # FormatDict manages missing fields in string formatting
+ return retstr % FormatDict(ent.data)
+
+ def _check_interval(self, interval):
+ '''
+ Check the interval for schedule replication is valid:
+ HH [0..23]
+ MM [0..59]
+ DAYS [0-6]{1,7}
+
+ @param interval - interval in the format 'HHMM-HHMM D+' (D is day
number [0-6])
+
+ @return None
+
+ @raise ValueError - if the inteval is illegal
+ '''
+ c = re.compile(re.compile('^([0-9][0-9])([0-9][0-9])-([0-9][0-9])([0-9][0-9])
([0-6]{1,7})$'))
+ if not c.match(interval):
+ raise ValueError("Bad schedule format %r" % interval)
+ schedule = c.split(interval, c.groups)
+
+ # check the hours
+ hour = int(schedule[1])
+ if ((hour < 0) or (hour > 23)):
+ raise ValueError("Bad schedule format %r: illegal hour %d" %
(interval, hour))
+ hour = int(schedule[3])
+ if ((hour < 0) or (hour > 23)):
+ raise ValueError("Bad schedule format %r: illegal hour %d" %
(interval, hour))
+ if int(schedule[1]) > int(schedule[3]):
+ raise ValueError("Bad schedule (start HOUR larger than end HOUR) %r:
illegal hour %d" % (interval, int(schedule[1])))
+
+ # check the minutes
+ minute = int(schedule[2])
+ if ((minute < 0) or (minute > 59)):
+ raise ValueError("Bad schedule format %r: illegal minute %d" %
(interval, minute))
+ minute = int(schedule[4])
+ if ((minute < 0) or (minute > 59)):
+ raise ValueError("Bad schedule format %r: illegal minute %d" %
(interval, minute))
+
+
+
+
+ def schedule(self, agmtdn=None, interval=ALWAYS):
+ """Schedule the replication agreement
+ @param agmtdn - DN of the replica agreement
+ @param interval - in the form
+ - Agreement.ALWAYS
+ - Agreement.NEVER
+ - or 'HHMM-HHMM D+' With D=[0123456]+
+
+ @return - None
+
+ @raise ValueError - if interval is not valid
+ ldap.NO_SUCH_OBJECT - if agmtdn does not exist
+ """
+ if not agmtdn:
+ raise InvalidArgumentError("agreement DN is missing")
+
+ # check the validity of the interval
+ if interval != Agreement.ALWAYS and interval != Agreement.NEVER:
+ self._check_interval(interval)
+
+ # Check if the replica agreement exists
+ try:
+ self.conn.getEntry(agmtdn, ldap.SCOPE_BASE)
+ except ldap.NO_SUCH_OBJECT:
+ raise
+
+ # update it
+ self.log.info("Schedule replication agreement %s" % agmtdn)
+ mod = [(
+ ldap.MOD_REPLACE, 'nsds5replicaupdateschedule', [interval])]
+ self.conn.modify_s(agmtdn, mod)
+
+ def getProperties(self, agmnt_dn=None, properties=None):
+ '''
+ returns a dictionary of the requested properties. If properties is missing,
+ it returns all the properties.
+ @param agmtdn - is the replica agreement DN
+ @param properties - is the list of properties name
+ Supported properties are
+ RA_NAME
+ RA_SUFFIX
+ RA_BINDDN
+ RA_BINDPW
+ RA_METHOD
+ RA_DESCRIPTION
+ RA_SCHEDULE
+ RA_TRANSPORT_PROT
+ RA_FRAC_EXCLUDE
+ RA_FRAC_EXCLUDE_TOTAL_UPDATE
+ RA_FRAC_STRIP
+ RA_CONSUMER_PORT
+ RA_CONSUMER_HOST
+ RA_CONSUMER_TOTAL_INIT
+ RA_TIMEOUT
+ RA_CHANGES
+
+ @return - returns a dictionary of the properties
+
+ @raise ValueError - if invalid property name
+ ldap.NO_SUCH_OBJECT - if agmtdn does not exist
+ InvalidArgumentError - missing mandatory argument
+
+
+ '''
+
+ if not agmnt_dn:
+ raise InvalidArgumentError("agmtdn is a mandatory argument")
+
+ #
+ # prepare the attribute list to retrieve from the RA
+ # if properties is None, all RA attributes are retrieved
+ #
+ attrs = []
+ if properties:
+ for prop_name in properties:
+ prop_attr = RA_PROPNAME_TO_ATTRNAME[prop_name]
+ if not prop_attr:
+ raise ValueError("Improper property name: %s ", prop_name)
+ attrs.append(prop_attr)
+
+ filt = "(objectclass=*)"
+ result = {}
+ try:
+ entry = self.conn.getEntry(agmnt_dn, ldap.SCOPE_BASE, filt, attrs)
+
+ # Build the result from the returned attributes
+ for attr in entry.getAttrs():
+ # given an attribute name retrieve the property name
+ props = [ k for k, v in RA_PROPNAME_TO_ATTRNAME.iteritems() if v.lower()
== attr.lower() ]
+
+ # If this attribute is present in the RA properties, adds it to result
+ if len(props) > 0:
+ result[props[0]] = entry.getValues(attr)
+
+ except ldap.NO_SUCH_OBJECT:
+ raise
+
+ return result
+
+ def setProperties(self, suffix=None, agmnt_dn=None, agmnt_entry=None,
properties=None):
+ '''
+ Set the properties of the agreement. If an 'agmnt_entry' (Entry) is
provided, it updates the entry, else
+ it updates the entry on the server. If the 'agmnt_dn' is provided it
retrieves the entry using it, else
+ it retrieve the agreement using the 'suffix'.
+
+ @param suffix : suffix stored in that agreement (online update)
+ @param agmnt_dn: DN of the agreement (online update)
+ @param agmnt_entry: Entry of a agreement (offline update)
+ @param properties: dictionary of properties
+ Supported properties are
+ RA_NAME
+ RA_SUFFIX
+ RA_BINDDN
+ RA_BINDPW
+ RA_METHOD
+ RA_DESCRIPTION
+ RA_SCHEDULE
+ RA_TRANSPORT_PROT
+ RA_FRAC_EXCLUDE
+ RA_FRAC_EXCLUDE_TOTAL_UPDATE
+ RA_FRAC_STRIP
+ RA_CONSUMER_PORT
+ RA_CONSUMER_HOST
+ RA_CONSUMER_TOTAL_INIT
+ RA_TIMEOUT
+ RA_CHANGES
+
+
+ @return None
+
+ @raise ValueError: if unknown properties
+ ValueError: if invalid agreement_entry
+ ValueError: if agmnt_dn or suffix are not associated to a replica
+ InvalidArgumentError: If missing mandatory parameter
+
+
+ '''
+ # No properties provided
+ if len(properties) == 0:
+ return
+
+ # check that the given properties are valid
+ for prop in properties:
+ # skip the prefix to add/del value
+ if not inProperties(prop, RA_PROPNAME_TO_ATTRNAME):
+ raise ValueError("unknown property: %s" % prop)
+ else:
+ self.log.debug("setProperties: %s:%s" % (prop,
properties[prop]))
+
+ # At least we need to have suffix/agmnt_dn/agmnt_entry
+ if not suffix and not agmnt_dn and not agmnt_entry:
+ raise InvalidArgumentError("suffix and agmnt_dn and agmnt_entry are
missing")
+
+ # TODO
+ if suffix:
+ raise NotImplemented
+
+ # the caller provides a set of properties to set into a replica entry
+ if agmnt_entry:
+ if not isinstance(agmnt_entry, Entry):
+ raise ValueError("invalid instance of the agmnt_entry")
+
+ # that is fine, now set the values
+ for prop in properties:
+ val = rawProperty(prop)
+
+ # for Entry update it is a replace
+ agmnt_entry.update({RA_PROPNAME_TO_ATTRNAME[val]: properties[prop]})
+
+ return
+
+ # for each provided property build the mod
+ mod = []
+ for prop in properties:
+
+ # retrieve/check the property name
+ # and if the value needs to be added/deleted/replaced
+ if prop.startswith('+'):
+ mod_type = ldap.MOD_ADD
+ prop_name = prop[1:]
+ elif prop.startswith('-'):
+ mod_type = ldap.MOD_DELETE
+ prop_name = prop[1:]
+ else:
+ mod_type = ldap.MOD_REPLACE
+ prop_name = prop
+
+ attr = RA_PROPNAME_TO_ATTRNAME[prop_name]
+ if not attr:
+ raise ValueError("invalid property name %s" % prop_name)
+
+ # Now do the value checking we can do
+ if prop_name == RA_SCHEDULE:
+ self._check_interval(properties[prop])
+
+ mod.append((mod_type, attr, properties[prop]))
+
+ # Now time to run the effective modify
+ self.conn.modify_s(agmnt_dn, mod)
+
+
+ def list(self, suffix=None, consumer_host=None, consumer_port=None, agmtdn=None):
+ '''
+ Returns the search result of the replica agreement(s) under the replica
(replicaRoot is 'suffix').
+
+ Either 'suffix' or 'agmtdn' need to be specfied.
+ 'consumer_host' and 'consumer_port' are either not specified
or specified both.
+
+ If 'agmtdn' is specified, it returns the search result entry of that
replication agreement.
+ else if consumer host/port are specified it returns the replica agreements
toward
+ that consumer host:port.
+ Finally if neither 'agmtdn' nor 'consumser host/port' are
specifies it returns
+ all the replica agreements under the replica (replicaRoot is
'suffix').
+
+ @param - suffix is the suffix targeted by the total update
+ @param - consumer_host hostname of the consumer
+ @param - consumer_port port of the consumer
+ @param - agmtdn DN of the replica agreement
+
+ @return - search result of the replica agreements
+
+ @raise - InvalidArgument: if missing mandatory argument (agmtdn or suffix,
then host and port)
+ - ValueError - if some properties are not valid
+ - NoSuchEntryError - If no replica defined for the suffix
+ '''
+ if not suffix and not agmtdn:
+ raise InvalidArgumentError("suffix or agmtdn are required")
+
+ if (consumer_host and not consumer_port) or (not consumer_host and
consumer_port):
+ raise InvalidArgumentError("consumer host/port are required
together")
+
+ if agmtdn:
+ # easy case, just return the RA
+ filt = "objectclass=*"
+ return self.conn.search_s(agmtdn, ldap.SCOPE_BASE, filt)
+ else:
+ # Retrieve the replica
+ replica_entries = self.conn.replica.list(suffix)
+ if not replica_entries:
+ raise NoSuchEntryError("Error: no replica set up for suffix " +
suffix)
+ replica_entry = replica_entries[0]
+
+ # Now returns the replica agreement for that suffix that replicates to
+ # consumer host/port
+ if consumer_host and consumer_port:
+ filt = "(&(objectclass=%s)(%s=%s)(%s=%d))" %
(RA_OBJECTCLASS_VALUE,
+
RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_HOST], consumer_host,
+
RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_PORT], consumer_port)
+ else:
+ filt = "(objectclass=%s)" % RA_OBJECTCLASS_VALUE
+ return self.conn.search_s(replica_entry.dn, ldap.SCOPE_ONELEVEL, filt)
+
+
+ def create(self, suffix=None, host=None, port=None, properties=None):
+ """Create (and return) a replication agreement from self to
consumer.
+ - self is the supplier,
+
+ @param consumer: one of the following (consumer can be a master)
+ * a DirSrv object if chaining
+ * an object with attributes: host, port, sslport, __str__
+ @param suffix - eg. 'dc=babel,dc=it'
+ @param properties - further properties dict.
+ Support properties
+ RA_NAME
+ RA_SUFFIX
+ RA_BINDDN
+ RA_BINDPW
+ RA_METHOD
+ RA_DESCRIPTION
+ RA_SCHEDULE
+ RA_TRANSPORT_PROT
+ RA_FRAC_EXCLUDE
+ RA_FRAC_EXCLUDE_TOTAL_UPDATE
+ RA_FRAC_STRIP
+ RA_CONSUMER_PORT
+ RA_CONSUMER_HOST
+ RA_CONSUMER_TOTAL_INIT
+ RA_TIMEOUT
+ RA_CHANGES
+
+
+ @return dn_agreement - DN of the created agreement
+
+ @raise InvalidArgumentError - If the suffix is missing
+ @raise NosuchEntryError - if a replica doesn't exist for that suffix
+ @raise UNWILLING_TO_PERFORM if the database was previously
+ in read-only state. To create new agreements you
+ need to *restart* the directory server
+
+ """
+ import string
+
+ # Check we have a suffix [ mandatory ]
+ if not suffix:
+ self.log.warning("create: suffix is missing")
+ raise InvalidArgumentError('suffix is mandatory')
+
+ if properties:
+ binddn = properties.get(RA_BINDDN) or
defaultProperties[REPLICATION_BIND_DN]
+ bindpw = properties.get(RA_BINDPW) or
defaultProperties[REPLICATION_BIND_PW]
+ bindmethod = properties.get(RA_METHOD) or
defaultProperties[REPLICATION_BIND_METHOD]
+ format = properties.get(RA_NAME) or
r'meTo_$host:$port'
+ description = properties.get(RA_DESCRIPTION) or format
+ transport = properties.get(RA_TRANSPORT_PROT) or
defaultProperties[REPLICATION_TRANSPORT]
+ timeout = properties.get(RA_TIMEOUT) or
defaultProperties[REPLICATION_TIMEOUT]
+ else:
+ binddn = defaultProperties[REPLICATION_BIND_DN]
+ bindpw = defaultProperties[REPLICATION_BIND_PW]
+ bindmethod = defaultProperties[REPLICATION_BIND_METHOD]
+ format = r'meTo_$host:$port'
+ description = format
+ transport = defaultProperties[REPLICATION_TRANSPORT]
+ timeout = defaultProperties[REPLICATION_TIMEOUT]
+
+ # Compute the normalized suffix to be set in RA entry
+ nsuffix = normalizeDN(suffix)
+
+ # adding agreement under the replica entry
+ replica_entries = self.conn.replica.list(suffix)
+ if not replica_entries:
+ raise NoSuchEntryError(
+ "Error: no replica set up for suffix " + suffix)
+ replica = replica_entries[0]
+
+ # define agreement entry
+ cn = string.Template(format).substitute({'host': host, 'port':
port})
+ dn_agreement = ','.join(["cn=%s" % cn, replica.dn])
+
+ # This is probably unnecessary because
+ # we can just raise ALREADY_EXISTS
+ try:
+
+ entry = self.conn.getEntry(dn_agreement, ldap.SCOPE_BASE)
+ self.log.warn("Agreement already exists: %r" % dn_agreement)
+ return dn_agreement
+ except ldap.NO_SUCH_OBJECT:
+ entry = None
+
+ # In a separate function in this scope?
+ entry = Entry(dn_agreement)
+ entry.update({
+ 'objectclass': ["top", RA_OBJECTCLASS_VALUE],
+ RA_PROPNAME_TO_ATTRNAME[RA_NAME]: cn,
+ RA_PROPNAME_TO_ATTRNAME[RA_SUFFIX]: nsuffix,
+ RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_HOST]: host,
+ RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_PORT]: str(port),
+ RA_PROPNAME_TO_ATTRNAME[RA_TRANSPORT_PROT]: transport,
+ RA_PROPNAME_TO_ATTRNAME[RA_TIMEOUT]: str(timeout),
+ RA_PROPNAME_TO_ATTRNAME[RA_BINDDN]: binddn,
+ RA_PROPNAME_TO_ATTRNAME[RA_BINDPW]: bindpw,
+ RA_PROPNAME_TO_ATTRNAME[RA_METHOD]: bindmethod,
+ RA_PROPNAME_TO_ATTRNAME[RA_DESCRIPTION]:
string.Template(description).substitute({'host': host, 'port': port})
+ })
+
+ # we make a copy here because we cannot change
+ # the passed in properties dict
+ propertiescopy = {}
+ if properties:
+ import copy
+ propertiescopy = copy.deepcopy(properties)
+
+ # further arguments
+ if 'winsync' in propertiescopy: # state it clearly!
+ self.conn.setupWinSyncAgmt(propertiescopy, entry)
+
+ try:
+ self.log.debug("Adding replica agreement: [%r]" % entry)
+ self.conn.add_s(entry)
+ except:
+ # FIXME check please!
+ raise
+
+ entry = self.conn.waitForEntry(dn_agreement)
+ if entry:
+ # More verbose but shows what's going on
+ if 'chain' in propertiescopy:
+ raise NotImplementedError
+ chain_args = {
+ 'suffix': suffix,
+ 'binddn': binddn,
+ 'bindpw': bindpw
+ }
+ # Work on `self` aka producer
+ if replica.nsds5replicatype == MASTER_TYPE:
+ self.conn.setupChainingFarm(**chain_args)
+ # Work on `consumer`
+ # TODO - is it really required?
+ if replica.nsds5replicatype == LEAF_TYPE:
+ chain_args.update({
+ 'isIntermediate': 0,
+ 'urls': self.conn.toLDAPURL(),
+ 'args': propertiescopy['chainargs']
+ })
+ consumer.setupConsumerChainOnUpdate(**chain_args)
+ elif replica.nsds5replicatype == HUB_TYPE:
+ chain_args.update({
+ 'isIntermediate': 1,
+ 'urls': self.conn.toLDAPURL(),
+ 'args': propertiescopy['chainargs']
+ })
+ consumer.setupConsumerChainOnUpdate(**chain_args)
+
+ return dn_agreement
+
+
+
+ def init(self, suffix=None, consumer_host=None, consumer_port=None):
+ """Trigger a total update of the consumer replica
+ - self is the supplier,
+ - consumer is a DirSrv object (consumer can be a master)
+ - cn_format - use this string to format the agreement name
+ @param - suffix is the suffix targeted by the total update [mandatory]
+ @param - consumer_host hostname of the consumer [mandatory]
+ @param - consumer_port port of the consumer [mandatory]
+
+ @raise InvalidArgument: if missing mandatory argurment (suffix/host/port)
+ """
+ #
+ # check the required parameters are set
+ #
+ if not suffix:
+ self.log.fatal("initAgreement: suffix is missing")
+ raise InvalidArgumentError('suffix is mandatory argument')
+
+ nsuffix = normalizeDN(suffix)
+
+ if not consumer_host:
+ self.log.fatal("initAgreement: host is missing")
+ raise InvalidArgumentError('host is mandatory argument')
+
+ if not consumer_port:
+ self.log.fatal("initAgreement: port is missing")
+ raise InvalidArgumentError('port is mandatory argument')
+ #
+ # check the replica agreement already exist
+ #
+ replica_entries = self.conn.replica.list(suffix)
+ if not replica_entries:
+ raise NoSuchEntryError(
+ "Error: no replica set up for suffix " + suffix)
+ replica_entry = replica_entries[0]
+ self.log.debug("initAgreement: looking for replica agreements under %s"
% replica_entry.dn)
+ try:
+ filt =
"(&(objectclass=nsds5replicationagreement)(nsds5replicahost=%s)(nsds5replicaport=%d)(nsds5replicaroot=%s))"
% (consumer_host, consumer_port, nsuffix)
+ entry = self.conn.getEntry(replica_entry.dn, ldap.SCOPE_ONELEVEL, filt)
+ except ldap.NO_SUCH_OBJECT:
+ self.log.fatal("initAgreement: No replica agreement to %s:%d for suffix
%s" % (consumer_host, consumer_port, nsuffix))
+ raise
+
+ #
+ # trigger the total init
+ #
+ self.log.info("Starting total init %s" % entry.dn)
+ mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')]
+ self.conn.modify_s(entry.dn, mod)
+
+ def pause(self, agmtdn, interval=NEVER):
+ """Pause this replication agreement. This replication agreement
+ will send no more changes. Use the resume() method to "unpause".
+ It tries to disable the replica agreement. If it fails (not implemented in all
version),
+ it uses the schedule() with interval '2358-2359 0'
+ @param agmtdn - agreement dn
+ @param interval - (default NEVER) replication schedule to use
+
+ @return None
+
+ @raise ValueError - if interval is not valid
+ """
+ self.log.info("Pausing replication %s" % agmtdn)
+ mod = [(
+ ldap.MOD_REPLACE, 'nsds5ReplicaEnabled', ['off'])]
+ try:
+ self.conn.modify_s(agmtdn, mod)
+ except ldap.LDAPError, e:
+ # before 1.2.11, no support for nsds5ReplicaEnabled
+ # use schedule hack
+ self.schedule(interval)
+
+ def resume(self, agmtdn, interval=ALWAYS):
+ """Resume a paused replication agreement, paused with the
"pause" method.
+ It tries to enabled the replica agreement. If it fails (not implemented in all
version),
+ it uses the schedule() with interval '0000-2359 0123456'
+ @param agmtdn - agreement dn
+ @param interval - (default ALWAYS) replication schedule to use
+
+ @return None
+
+ @raise ValueError - if interval is not valid
+ ldap.NO_SUCH_OBJECT - if agmtdn does not exist
+ """
+ self.log.info("Resuming replication %s" % agmtdn)
+ mod = [(
+ ldap.MOD_REPLACE, 'nsds5ReplicaEnabled', ['on'])]
+ try:
+ self.conn.modify_s(agmtdn, mod)
+ except ldap.LDAPError, e:
+ # before 1.2.11, no support for nsds5ReplicaEnabled
+ # use schedule hack
+ self.schedule(interval)
+
+ def changes(self, agmnt_dn):
+ """Return a list of changes sent by this
agreement."""
+ retval = 0
+ try:
+ ent = self.conn.getEntry(
+ agmnt_dn, ldap.SCOPE_BASE, "(objectclass=*)", [
RA_PROPNAME_TO_ATTRNAME[RA_CHANGES] ])
+ except:
+ raise NoSuchEntryError(
+ "Error reading status from agreement", agmnt_dn)
+
+ if ent.nsds5replicaChangesSentSinceStartup:
+ val = ent.nsds5replicaChangesSentSinceStartup
+ items = val.split(' ')
+ if len(items) == 1:
+ retval = int(items[0])
+ else:
+ for item in items:
+ ary = item.split(":")
+ if ary and len(ary) > 1:
+ retval = retval + int(ary[1].split("/")[0])
+ return retval
+
\ No newline at end of file
diff --git a/src/lib389/lib389/brooker.py b/src/lib389/lib389/brooker.py
index 3d43009..1446978 100644
--- a/src/lib389/lib389/brooker.py
+++ b/src/lib389/lib389/brooker.py
@@ -24,804 +24,6 @@ from lib389 import (
from lib389._replication import RUV
from lib389._entry import FormatDict
-class Agreement(object):
- ALWAYS = '0000-2359 0123456'
- NEVER = '2358-2359 0'
-
- proxied_methods = 'search_s getEntry'.split()
-
- def __init__(self, conn):
- """@param conn - a DirSrv instance"""
- self.conn = conn
- self.log = conn.log
-
- def __getattr__(self, name):
- if name in Agreement.proxied_methods:
- return DirSrv.__getattr__(self.conn, name)
-
-
-
- def status(self, agreement_dn):
- """Return a formatted string with the replica status.
- @param agreement_dn -
- """
-
- attrlist = ['cn', 'nsds5BeginReplicaRefresh',
'nsds5replicaUpdateInProgress',
- 'nsds5ReplicaLastInitStatus',
'nsds5ReplicaLastInitStart',
- 'nsds5ReplicaLastInitEnd', 'nsds5replicaReapActive',
- 'nsds5replicaLastUpdateStart',
'nsds5replicaLastUpdateEnd',
- 'nsds5replicaChangesSentSinceStartup',
'nsds5replicaLastUpdateStatus',
- 'nsds5replicaChangesSkippedSinceStartup',
'nsds5ReplicaHost',
- 'nsds5ReplicaPort']
- try:
- ent = self.conn.getEntry(
- agreement_dn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist)
- except NoSuchEntryError:
- raise NoSuchEntryError(
- "Error reading status from agreement", agreement_dn)
- else:
- retstr = (
- "Status for %(cn)s agmt
%(nsDS5ReplicaHost)s:%(nsDS5ReplicaPort)s" "\n"
- "Update in progress: %(nsds5replicaUpdateInProgress)s"
"\n"
- "Last Update Start: %(nsds5replicaLastUpdateStart)s"
"\n"
- "Last Update End: %(nsds5replicaLastUpdateEnd)s"
"\n"
- "Num. Changes Sent: %(nsds5replicaChangesSentSinceStartup)s"
"\n"
- "Num. changes Skipped:
%(nsds5replicaChangesSkippedSinceStartup)s" "\n"
- "Last update Status: %(nsds5replicaLastUpdateStatus)s"
"\n"
- "Init in progress: %(nsds5BeginReplicaRefresh)s"
"\n"
- "Last Init Start: %(nsds5ReplicaLastInitStart)s"
"\n"
- "Last Init End: %(nsds5ReplicaLastInitEnd)s" "\n"
- "Last Init Status: %(nsds5ReplicaLastInitStatus)s"
"\n"
- "Reap Active: %(nsds5ReplicaReapActive)s" "\n"
- )
- # FormatDict manages missing fields in string formatting
- return retstr % FormatDict(ent.data)
-
- def _check_interval(self, interval):
- '''
- Check the interval for schedule replication is valid:
- HH [0..23]
- MM [0..59]
- DAYS [0-6]{1,7}
-
- @param interval - interval in the format 'HHMM-HHMM D+' (D is day
number [0-6])
-
- @raise ValueError - if the inteval is illegal
- '''
- c = re.compile(re.compile('^([0-9][0-9])([0-9][0-9])-([0-9][0-9])([0-9][0-9])
([0-6]{1,7})$'))
- if not c.match(interval):
- raise ValueError("Bad schedule format %r" % interval)
-
- # check the hours
- hour = int(c.group(1))
- if ((hour < 0) or (hour > 23)):
- raise ValueError("Bad schedule format %r: illegal hour %d" %
(interval, hour))
- hour = int(c.group(3))
- if ((hour < 0) or (hour > 23)):
- raise ValueError("Bad schedule format %r: illegal hour %d" %
(interval, hour))
-
- # check the minutes
- minute = int(c.group(2))
- if ((hour < 0) or (hour > 59)):
- raise ValueError("Bad schedule format %r: illegal minute %d" %
(interval, minute))
- minute = int(c.group(4))
- if ((hour < 0) or (hour > 59)):
- raise ValueError("Bad schedule format %r: illegal minute %d" %
(interval, minute))
-
-
-
-
- def schedule(self, agmtdn, interval=ALWAYS):
- """Schedule the replication agreement
- @param agmtdn - DN of the replica agreement
- @param interval - in the form
- - Agreement.ALWAYS
- - Agreement.NEVER
- - or 'HHMM-HHMM D+' With D=[0123456]+
- @raise ValueError - if interval is not valid
- """
-
- # check the validity of the interval
- if interval != Agreement.ALWAYS and interval != Agreement.NEVER:
- self._check_interval(interval)
-
- # Check if the replica agreement exists
- try:
- self.conn.getEntry(agmtdn, ldap.SCOPE_BASE)
- except ldap.NO_SUCH_OBJECT:
- raise
-
- # update it
- self.log.info("Schedule replication agreement %s" % agmtdn)
- mod = [(
- ldap.MOD_REPLACE, 'nsds5replicaupdateschedule', [interval])]
- self.conn.modify_s(agmtdn, mod)
-
- def checkProperties(self, entry, properties):
- '''
- Checks that properties defined in prop are valid and set the
- replica agreement entry with the corresponding RHDS attribute name
- @param entry - is the replica agreement LDAP entry
- @param properties - is the dict of <prop_name>:<value>
- 'schedule' ->
'nsds5replicaupdateschedule'
- 'transport-info' ->
'nsds5replicatransportinfo'
- 'fractional-exclude-attrs-inc' ->
'nsDS5ReplicatedAttributeList'
- 'fractional-exclude-attrs-total' ->
'nsDS5ReplicatedAttributeListTotal'
- 'fractional-strip-attrs' ->
'nsds5ReplicaStripAttrs'
- 'transport-prot' ->
'nsds5replicatransportinfo' [ 'LDAP' ]
- 'consumer-port' -> 'nsds5replicaport'
[ 389 ]
- 'consumer-total-init' ->
'nsds5BeginReplicaRefresh'
-
- @raise ValueError - if some properties are not valid
-
-
- '''
-
- #
- # Schedule replication
- #
- if 'schedule' in properties:
- self._check_interval(properties['schedule'])
- entry.update({'nsds5replicaupdateschedule':
properties['schedule']})
- if 'consumer-total-init' in properties:
- entry.setValues('nsds5BeginReplicaRefresh',
properties['consumer-total-init'])
-
- #
- # Fractional replication properties
- #
- if 'fractional-exclude-attrs-inc' in properties:
- entry.setValues('nsDS5ReplicatedAttributeList',
properties['fractional-exclude-attrs-inc'])
- if 'fractional-exclude-attrs-total' in properties:
- entry.setValues('nsDS5ReplicatedAttributeListTotal',
properties['fractional-exclude-attrs-total'])
- if 'fractional-strip-attrs' in properties:
- entry.setValues('nsds5ReplicaStripAttrs',
properties['fractional-strip-attrs'])
-
- #
- # Network parameter
- if 'transport-prot' in properties:
- entry.setValues('nsds5replicatransportinfo',
properties['transport-prot'])
- else:
- entry.setValues('nsds5replicatransportinfo', 'LDAP')
-
- if 'consumer-port' in properties:
- entry.setValues('nsds5replicaport',
properties['consumer-port'])
- else:
- entry.setValues('nsds5replicaport', 389)
-
-
-
- def create(self, consumer, suffix=None, binddn=None, bindpw=None,
cn_format=r'meTo_$host:$port', description_format=r'me to $host:$port',
timeout=120, auto_init=False, bindmethod='simple', starttls=False, args=None):
- """Create (and return) a replication agreement from self to
consumer.
- - self is the supplier,
-
- @param consumer: one of the following (consumer can be a master)
- * a DirSrv object if chaining
- * an object with attributes: host, port, sslport, __str__
- @param suffix - eg. 'dc=babel,dc=it'
- @param binddn -
- @param bindpw -
- @param cn_format - string.Template to format the agreement name
- @param timeout - replica timeout in seconds
- @param auto_init - start replication immediately
- @param bindmethod- 'simple'
- @param starttls - True or False
- @param args - further args dict. Allowed keys:
- 'schedule',
- 'fractional-exclude-attrs-inc',
- 'fractional-exclude-attrs-total',
- 'fractional-strip-attrs'
- 'winsync'
-
- @return dn_agreement - DN of the created agreement
-
- @raise InvalidArgumentError - If the suffix is missing
- @raise NosuchEntryError - if a replica doesn't exist for that suffix
- @raise UNWILLING_TO_PERFORM if the database was previously
- in read-only state. To create new agreements you
- need to *restart* the directory server
-
- NOTE: this method doesn't cache connection entries
-
- TODO: test winsync
- TODO: test chain
-
- """
- import string
-
- # Check we have a suffix [ mandatory ]
- if not suffix:
- self.log.warning("create: suffix is missing")
- raise InvalidArgumentError('suffix is mandatory')
-
- # Check we have a bindDN [optional] can be set later
- try:
- binddn = binddn or defaultProperties[REPLICATION_BIND_DN]
- if not binddn:
- raise KeyError
- except KeyError:
- self.log.warning("create: bind DN not specified")
- pass
-
- # Check we have a bindDN [optional] can be set later
- try:
- bindpw = bindpw or defaultProperties[REPLICATION_BIND_PW]
- if not bindpw:
- raise KeyError
- except KeyError:
- self.log.warning("create: %s password not specified" % (binddn))
- pass
-
- # Set the connection info HOST:PORT
- othhost, othport, othsslport = (
- consumer.host, consumer.port, consumer.sslport)
- othport = othsslport or othport
- othport = othport or 389
-
- # Compute the normalized suffix to be set in RA entry
- nsuffix = normalizeDN(suffix)
-
- # adding agreement under the replica entry
- replica_entries = self.conn.replica.list(suffix)
- if not replica_entries:
- raise NoSuchEntryError(
- "Error: no replica set up for suffix " + suffix)
- replica = replica_entries[0]
-
- # define agreement entry
- cn = string.Template(cn_format).substitute({'host': othhost,
'port': othport})
- dn_agreement = ','.join(["cn=%s" % cn, replica.dn])
-
- # This is probably unnecessary because
- # we can just raise ALREADY_EXISTS
- try:
-
- entry = self.conn.getEntry(dn_agreement, ldap.SCOPE_BASE)
- self.log.warn("Agreement already exists: %r" % dn_agreement)
- return dn_agreement
- except ldap.NO_SUCH_OBJECT:
- entry = None
-
- # In a separate function in this scope?
- entry = Entry(dn_agreement)
- entry.update({
- 'objectclass': ["top",
"nsds5replicationagreement"],
- 'cn': cn,
- 'nsds5replicahost': consumer.host,
- 'nsds5replicatimeout': str(timeout),
- 'nsds5replicabinddn': binddn,
- 'nsds5replicacredentials': bindpw,
- 'nsds5replicabindmethod': bindmethod,
- 'nsds5replicaroot': nsuffix,
- 'description':
string.Template(description_format).substitute({'host': othhost, 'port':
othport})
- })
-
- # prepare the properties to set in the agreement
- # we make a copy here because we cannot change
- # the passed in args dict
- argscopy = {}
- if args:
- import copy
- argscopy = copy.deepcopy(args)
- if 'transport-info' not in argscopy :
- if starttls:
- argscopy['transport-info'] = 'TLS'
- elif othsslport:
- argscopy['transport-info'] = 'SSL'
- else:
- argscopy['transport-info'] = 'LDAP'
- if 'consumer-port' not in argscopy:
- if starttls:
- argscopy['consumer-port'] = str(othport)
- elif othsslport:
- argscopy['consumer-port'] = str(othsslport)
- elif othport:
- argscopy['consumer-port'] = str(othport)
- else:
- argscopy['consumer-port'] = '389'
- if 'consumer-total-init' not in argscopy:
- if auto_init:
- argscopy['consumer-total-init'] = 'start'
-
- # now check and set the properties in the entry
- self.checkProperties(entry, argscopy)
-
-
- # further arguments
- if 'winsync' in argscopy: # state it clearly!
- self.conn.setupWinSyncAgmt(argscopy, entry)
-
- try:
- self.log.debug("Adding replica agreement: [%s]" % entry)
- self.conn.add_s(entry)
- except:
- # FIXME check please!
- raise
-
- entry = self.conn.waitForEntry(dn_agreement)
- if entry:
- # More verbose but shows what's going on
- if 'chain' in argscopy:
- chain_args = {
- 'suffix': suffix,
- 'binddn': binddn,
- 'bindpw': bindpw
- }
- # Work on `self` aka producer
- if replica.nsds5replicatype == MASTER_TYPE:
- self.setupChainingFarm(**chain_args)
- # Work on `consumer`
- # TODO - is it really required?
- if replica.nsds5replicatype == LEAF_TYPE:
- chain_args.update({
- 'isIntermediate': 0,
- 'urls': self.conn.toLDAPURL(),
- 'args': argscopy['chainargs']
- })
- consumer.setupConsumerChainOnUpdate(**chain_args)
- elif replica.nsds5replicatype == HUB_TYPE:
- chain_args.update({
- 'isIntermediate': 1,
- 'urls': self.conn.toLDAPURL(),
- 'args': argscopy['chainargs']
- })
- consumer.setupConsumerChainOnUpdate(**chain_args)
-
- return dn_agreement
-
- def init(self, suffix=None, consumer_host=None, consumer_port=None):
- """Trigger a total update of the consumer replica
- - self is the supplier,
- - consumer is a DirSrv object (consumer can be a master)
- - cn_format - use this string to format the agreement name
- @param - suffix is the suffix targeted by the total update [mandatory]
- @param - consumer_host hostname of the consumer [mandatory]
- @param - consumer_port port of the consumer [mandatory]
-
- @raise InvalidArgument: if missing mandatory argurment (suffix/host/port)
- """
- #
- # check the required parameters are set
- #
- if not suffix:
- self.log.fatal("initAgreement: suffix is missing")
- raise InvalidArgumentError('suffix is mandatory argument')
-
- nsuffix = normalizeDN(suffix)
-
- if not consumer_host:
- self.log.fatal("initAgreement: host is missing")
- raise InvalidArgumentError('host is mandatory argument')
-
- if not consumer_port:
- self.log.fatal("initAgreement: port is missing")
- raise InvalidArgumentError('port is mandatory argument')
- #
- # check the replica agreement already exist
- #
- replica_entries = self.conn.replica.list(suffix)
- if not replica_entries:
- raise NoSuchEntryError(
- "Error: no replica set up for suffix " + suffix)
- replica_entry = replica_entries[0]
- self.log.debug("initAgreement: looking for replica agreements under %s"
% replica_entry.dn)
- try:
- filt =
"(&(objectclass=nsds5replicationagreement)(nsds5replicahost=%s)(nsds5replicaport=%d)(nsds5replicaroot=%s))"
% (consumer_host, consumer_port, nsuffix)
- entry = self.conn.getEntry(replica_entry.dn, ldap.SCOPE_ONELEVEL, filt)
- except ldap.NO_SUCH_OBJECT:
- self.log.fatal("initAgreement: No replica agreement to %s:%d for suffix
%s" % (consumer_host, consumer_port, nsuffix))
- raise
-
- #
- # trigger the total init
- #
- self.log.info("Starting total init %s" % entry.dn)
- mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')]
- self.conn.modify_s(entry.dn, mod)
-
- def pause(self, agmtdn, interval=NEVER):
- """Pause this replication agreement. This replication agreement
- will send no more changes. Use the resume() method to "unpause"
- @param agmtdn - agreement dn
- @param interval - (default NEVER) replication schedule to use
- """
- self.log.info("Pausing replication %s" % agmtdn)
- mod = [(
- ldap.MOD_REPLACE, 'nsds5ReplicaEnabled', ['off'])]
- try:
- self.conn.modify_s(agmtdn, mod)
- except ldap.LDAPError:
- # before 1.2.11, no support for nsds5ReplicaEnabled
- # use schedule hack
- self.schedule(interval)
-
- def resume(self, agmtdn, interval=ALWAYS):
- """Resume a paused replication agreement, paused with the
"pause" method.
- @param agmtdn - agreement dn
- @param interval - (default ALWAYS) replication schedule to use
- """
- self.log.info("Resuming replication %s" % agmtdn)
- mod = [(
- ldap.MOD_REPLACE, 'nsds5ReplicaEnabled', ['on'])]
- try:
- self.conn.modify_s(agmtdn, mod)
- except ldap.LDAPError:
- # before 1.2.11, no support for nsds5ReplicaEnabled
- # use schedule hack
- self.schedule(interval)
-
-
-
-class Replica(object):
- proxied_methods = 'search_s getEntry'.split()
-
- def __init__(self, conn):
- """@param conn - a DirSrv instance"""
- self.conn = conn
- self.log = conn.log
-
- def __getattr__(self, name):
- if name in Replica.proxied_methods:
- return DirSrv.__getattr__(self.conn, name)
-
- def _get_mt_entry(self, suffix):
- """Return the replica dn of the given suffix."""
- mtent = self.conn.mappingtree.list(suffix=suffix)
- return ','.join(("cn=replica", mtent.dn))
-
- def create_repl_manager(self, repl_manager_dn=None, repl_manager_pw=None):
- '''
- Create an entry that will be used to bind as replica manager.
-
- @param repl_manager_dn - DN of the bind entry. If not provided use the
default one
- @param repl_manager_pw - Password of the entry. If not provide use the
default one
-
- @raise - KeyError if can not find valid values of Bind DN and Pwd
- '''
-
- # check the DN and PW
- try:
- repl_manager_dn = repl_manager_dn or defaultProperties[REPLICATION_BIND_DN]
- repl_manager_pw = repl_manager_pw or defaultProperties[REPLICATION_BIND_PW]
- if not repl_manager_dn or not repl_manager_pw:
- raise KeyError
- except KeyError:
- if not repl_manager_pw:
- self.log.warning("replica_createReplMgr: bind DN password not
specified")
- if not repl_manager_dn:
- self.log.warning("replica_createReplMgr: bind DN not
specified")
- raise
-
- # if the replication manager entry already exists, ust return
- try:
- entries = self.search_s(repl_manager_dn, ldap.SCOPE_BASE,
"objectclass=*")
- if entries:
- #it already exist, fine
- return
- except ldap.NO_SUCH_OBJECT:
- pass
-
- # ok it does not exist, create it
- try:
- attrs = {
- 'nsIdleTimeout': '0',
- 'passwordExpirationTime': '20381010000000Z'
- }
- self.conn.setupBindDN(repl_manager_dn, repl_manager_pw, attrs)
- except ldap.ALREADY_EXISTS:
- self.log.warn("User already exists (weird we just checked: %s " %
repl_manager_dn)
-
-
- def changelog(self, dbname='changelogdb'):
- """Add and return the replication changelog entry.
-
- If dbname starts with "/" then it's considered a full path,
- otherwise it's relative to self.dbdir
- """
- dn = DN_CHANGELOG
- dirpath = os.path.join(self.conn.dbdir, dbname)
- entry = Entry(dn)
- entry.update({
- 'objectclass': ("top", "extensibleobject"),
- 'cn': "changelog5",
- 'nsslapd-changelogdir': dirpath
- })
- self.log.debug("adding changelog entry: %r" % entry)
- self.changelogdir = dirpath
- try:
- self.conn.add_s(entry)
- except ldap.ALREADY_EXISTS:
- self.log.warn("entry %s already exists" % dn)
-
- return self.conn._test_entry(dn, ldap.SCOPE_BASE)
-
- def changelog_config(self, maxage=0, maxentries=0, trimInterval=0,
compactInterval=0):
- mod = []
- if maxage:
- mod.append((ldap.MOD_REPLACE, "nsslapd-changelogmaxage",
str(maxage)))
- if maxentries:
- mod.append((ldap.MOD_REPLACE, "nsslapd-changelogmaxentries",
str(maxentries)))
- if trimInterval:
- mod.append((ldap.MOD_REPLACE, "nsslapd-changelogtrim-interval",
str(trimInterval)))
- if compactInterval:
- mod.append((ldap.MOD_REPLACE,
"nsslapd-changelogcompactdb-interval", str(compactInterval)))
- if mod and len(mod) > 0:
- self.conn.modify_s(DN_CHANGELOG, mod)
-
- def list(self, suffix=None):
- """Return a list of replica entries under the given suffix.
- @param suffix - if suffix is None, return all replicas
- """
- if suffix:
- filtr = "(&(objectclass=nsds5Replica)(nsds5replicaroot=%s))" %
suffix
- else:
- filtr = "(objectclass=nsds5Replica)"
- ents = self.conn.search_s(DN_MAPPING_TREE, ldap.SCOPE_SUBTREE, filtr)
- return ents
-
- def check_init(self, agmtdn):
- """returns tuple - first element is done/not done, 2nd is no
error/has error
- @param agmtdn - the agreement dn
- """
- done, hasError = False, 0
- attrlist = ['cn', 'nsds5BeginReplicaRefresh',
'nsds5replicaUpdateInProgress',
- 'nsds5ReplicaLastInitStatus',
'nsds5ReplicaLastInitStart',
- 'nsds5ReplicaLastInitEnd']
- try:
- entry = self.conn.getEntry(
- agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist)
- except NoSuchEntryError:
- self.log.exception("Error reading status from agreement %r" %
agmtdn)
- hasError = 1
- else:
- refresh = entry.nsds5BeginReplicaRefresh
- inprogress = entry.nsds5replicaUpdateInProgress
- status = entry.nsds5ReplicaLastInitStatus
- if not refresh: # done - check status
- if not status:
- print "No status yet"
- elif status.find("replica busy") > -1:
- print "Update failed - replica busy - status", status
- done = True
- hasError = 2
- elif status.find("Total update succeeded") > -1:
- print "Update succeeded: status ", status
- done = True
- elif inprogress.lower() == 'true':
- print "Update in progress yet not in progress: status ",
status
- else:
- print "Update failed: status", status
- hasError = 1
- done = True
- elif self.verbose:
- print "Update in progress: status", status
-
- return done, hasError
-
- def start_and_wait(self, agmtdn):
- """@param agmtdn - agreement dn"""
- rc = self.start_async(agmtdn)
- if not rc:
- rc = self.wait_init(agmtdn)
- if rc == 2: # replica busy - retry
- rc = self.start_and_wait(agmtdn)
- return rc
-
- def wait_init(self, agmtdn):
- """Initialize replication and wait for completion.
- @oaram agmtdn - agreement dn
- """
- done = False
- haserror = 0
- while not done and not haserror:
- time.sleep(1) # give it a few seconds to get going
- done, haserror = self.check_init(agmtdn)
- return haserror
-
- def start_async(self, agmtdn):
- """Initialize replication without waiting.
- @param agmtdn - agreement dn
- """
- self.log.info("Starting async replication %s" % agmtdn)
- mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')]
- self.conn.modify_s(agmtdn, mod)
-
-
- def keep_in_sync(self, agmtdn):
- """
- @param agmtdn -
- """
- self.log.info("Setting agreement for continuous replication")
- raise NotImplementedError("Check nsds5replicaupdateschedule before
writing!")
-
-
-
- def add(self, suffix, binddn, bindpw=None, role=None, rid=None,
tombstone_purgedelay=None, purgedelay=None, referrals=None, legacy=False):
- """Setup a replica entry on an existing suffix.
- @param suffix - dn of suffix
- @param binddn - the replication bind dn for this replica
- can also be a list
["cn=r1,cn=config","cn=r2,cn=config"]
- @param bindpw - used to eventually provision the replication entry
-
- @param role - REPLICAROLE_MASTER, REPLICAROLE_HUB or REPLICAROLE_CONSUMER
- @param rid - replica id or - if not given - an internal sequence number will
be assigned
-
- # further args
- @param legacy - true or false - for legacy consumer
- @param tombstone_purgedelay
- @param purgedelay - changelog expiration time in seconds
- @param referrals
-
- Ex. replica.add(**{
- 'suffix': "dc=example,dc=com",
- 'type' : lib389.MASTER_TYPE,
- 'binddn': "cn=replication manager,cn=config"
- })
- binddn
- TODO: this method does not update replica type
- """
- # Check validity of role
- if not role:
- self.log.fatal("Replica.create: replica role not specify
(REPLICAROLE_*)")
- raise ValueError("role missing")
-
- if role != REPLICAROLE_MASTER and role != REPLICAROLE_HUB and role !=
REPLICAROLE_CONSUMER:
- self.log.fatal("enableReplication: replica role invalid (%s) " %
role)
- raise ValueError("invalid role: %s" % role)
-
- # role is fine, set the replica type
- if role == REPLICAROLE_MASTER:
- rtype = REPLICA_RDWR_TYPE
- else:
- rtype = REPLICA_RDONLY_TYPE
-
- if legacy:
- legacy = 'on'
- else:
- legacy = 'off'
-
- # create replica entry in mapping-tree
- nsuffix = normalizeDN(suffix)
- mtents = self.conn.mappingtree.list(suffix=suffix)
- mtent = mtents[0]
- dn_replica = ','.join(("cn=replica", mtent.dn))
- try:
- entry = self.conn.getEntry(dn_replica, ldap.SCOPE_BASE)
- self.log.warn("Already setup replica for suffix %r" % suffix)
- rec = self.conn.suffixes.setdefault(nsuffix, {})
- rec.update({'dn': dn_replica, 'type': rtype})
- return rec
- except ldap.NO_SUCH_OBJECT:
- entry = None
-
- # If a replica does not exist
- binddnlist = []
- if hasattr(binddn, '__iter__'):
- binddnlist = binddn
- else:
- binddnlist.append(binddn)
-
- entry = Entry(dn_replica)
- entry.update({
- 'objectclass': ("top", "nsds5replica",
"extensibleobject"),
- 'cn': "replica",
- 'nsds5replicaroot': nsuffix,
- 'nsds5replicaid': str(rid),
- 'nsds5replicatype': str(rtype),
- 'nsds5replicalegacyconsumer': legacy,
- 'nsds5replicabinddn': binddnlist
- })
- if role != REPLICAROLE_CONSUMER:
- entry.setValues('nsds5flags', "1")
-
- # other args
- if tombstone_purgedelay is not None:
- entry.setValues(
- 'nsds5replicatombstonepurgeinterval', str(tombstone_purgedelay))
- if purgedelay is not None:
- entry.setValues('nsds5ReplicaPurgeDelay', str(purgedelay))
- if referrals:
- entry.setValues('nsds5ReplicaReferral', referrals)
-
- self.conn.add_s(entry)
-
- # check if the entry exists TODO better to raise!
- self.conn._test_entry(dn_replica, ldap.SCOPE_BASE)
-
- self.conn.suffixes[nsuffix] = {'dn': dn_replica, 'type': rtype}
- return {'dn': dn_replica, 'type': rtype}
-
- def ruv(self, suffix, tryrepl=False):
- """return a replica update vector for the given suffix.
-
- @param suffix - eg. 'o=netscapeRoot'
-
- @raises NoSuchEntryError if missing
- """
- uuid = "ffffffff-ffffffff-ffffffff-ffffffff"
- filt = "(&(nsUniqueID=%s)(objectclass=nsTombstone))" % uuid
- attrs = ['nsds50ruv', 'nsruvReplicaLastModified']
- ents = self.conn.search_s(suffix, ldap.SCOPE_SUBTREE, filt, attrs)
- ent = None
- if ents and (len(ents) > 0):
- ent = ents[0]
- elif tryrepl:
- self.log.warn("Could not get RUV from %r entry - trying cn=replica"
% suffix)
- ensuffix = escapeDNValue(normalizeDN(suffix))
- dn = ','.join(("cn=replica", "cn=%s" % ensuffix,
DN_MAPPING_TREE))
- ents = self.conn.search_s(dn, ldap.SCOPE_BASE, "objectclass=*",
attrs)
-
- if ents and (len(ents) > 0):
- ent = ents[0]
- self.log.debug("RUV entry is %r" % ent)
- return RUV(ent)
-
- raise NoSuchEntryError("RUV not found: suffix: %r" % suffix)
-
- def agreements(self, filtr='', attrs=None, dn=True):
- """Return a list of agreement dn.
- @param filtr - get only agreements matching the given filter
- eg. '(cn=*example.it*)'
- @param attrs - attributes to retrieve
- eg. use ['*'] for all, defaul is ['cn']
- @param dn - return a list of lib389.Entry if dn=False
-
- """
- attrs = attrs or ['cn']
- realfiltr = "(objectclass=nsds5ReplicationAgreement)"
- if filtr:
- realfiltr = "(&%s%s)" % (realfiltr, filtr)
-
- ents = self.conn.search_s(
- DN_MAPPING_TREE, ldap.SCOPE_SUBTREE, realfiltr, attrs)
- if dn:
- return [ent.dn for ent in ents]
- return ents
-
- def agreement_dn(self, basedn, oth):
- '''get the replication agreement to the dirsrv object
- specified by oth for the suffix basedn
- this returns the replication agreement handle in a form suitable
- for passing to agreement_changes, start_async, etc.'''
- filt = '(nsds5replicahost=%s)(nsds5replicaport=%d)(nsds5replicaroot=%s)'
% \
- (oth.host, oth.port, normalizeDN(basedn))
- dns = self.agreements(filt, None, True)
- if not dns:
- self.log.error('no agreement found for host %s port %d suffix %s',
oth.host, oth.port, basedn)
- return None
- elif len(dns) > 1:
- self.log.error('%d agreements found for host %s port %d suffix %s',
len(dns), oth.host, oth.port, basedn)
- return None
- else:
- return dns[0]
-
-
-
-
-
-
- def agreement_changes(self, agmtdn):
- """Return a list of changes sent by this
agreement."""
- retval = 0
- try:
- ent = self.conn.getEntry(
- agmtdn, ldap.SCOPE_BASE, "(objectclass=*)",
- ['nsds5replicaChangesSentSinceStartup'])
- except:
- raise NoSuchEntryError(
- "Error reading status from agreement", agmtdn)
-
- if ent.nsds5replicaChangesSentSinceStartup:
- val = ent.nsds5replicaChangesSentSinceStartup
- items = val.split(' ')
- if len(items) == 1:
- retval = int(items[0])
- else:
- for item in items:
- ary = item.split(":")
- if ary and len(ary) > 1:
- retval = retval + int(ary[1].split("/")[0])
- return retval
-
class Config(object):
"""
diff --git a/src/lib389/lib389/changelog.py b/src/lib389/lib389/changelog.py
new file mode 100644
index 0000000..53c5592
--- /dev/null
+++ b/src/lib389/lib389/changelog.py
@@ -0,0 +1,96 @@
+'''
+Created on Jan 6, 2014
+
+@author: tbordaz
+'''
+
+import ldap
+import os
+from lib389 import DirSrv, Entry, InvalidArgumentError
+from lib389._constants import *
+from lib389.properties import *
+
+
+class Changelog(object):
+ proxied_methods = 'search_s getEntry'.split()
+
+ def __init__(self, conn):
+ """@param conn - a DirSrv instance"""
+ self.conn = conn
+ self.log = conn.log
+
+ def __getattr__(self, name):
+ if name in Changelog.proxied_methods:
+ return DirSrv.__getattr__(self.conn, name)
+
+ def list(self, suffix=None, changelogdn=None):
+ if not changelogdn:
+ raise InvalidArgumentError("changelog DN is missing")
+
+ base = changelogdn
+ filtr = "(objectclass=extensibleobject)"
+
+ # now do the effective search
+ ents = self.conn.search_s(base, ldap.SCOPE_BASE, filtr)
+ return ents
+
+ def create(self, dbname=DEFAULT_CHANGELOG_DB):
+ """Add and return the replication changelog entry.
+
+ If dbname starts with "/" then it's considered a full path,
+ otherwise it's relative to self.dbdir
+ """
+ dn = DN_CHANGELOG
+ attribute, changelog_name = dn.split(",")[0].split("=", 1)
+ dirpath = os.path.join(self.conn.dbdir, dbname)
+ entry = Entry(dn)
+ entry.update({
+ 'objectclass': ("top", "extensibleobject"),
+ CHANGELOG_PROPNAME_TO_ATTRNAME[CHANGELOG_NAME]: changelog_name,
+ CHANGELOG_PROPNAME_TO_ATTRNAME[CHANGELOG_DIR]: dirpath
+ })
+ self.log.debug("adding changelog entry: %r" % entry)
+ self.changelogdir = dirpath
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.warn("entry %s already exists" % dn)
+ return(dn)
+
+ def delete(self, changelogdn=None):
+ raise NotImplemented
+
+ def setProperties(self, changelogdn=None, properties=None):
+ if not changelogdn:
+ raise InvalidArgumentError("changelog DN is missing")
+
+ ents = self.changelog.list(changelogdn=changelogdn)
+ if len(ents) != 1:
+ raise ValueError("Changelog entry not found: %s" % changelogdn)
+
+ # check that the given properties are valid
+ for prop in properties:
+ # skip the prefix to add/del value
+ if not inProperties(prop, CHANGELOG_PROPNAME_TO_ATTRNAME):
+ raise ValueError("unknown property: %s" % prop)
+
+ # build the MODS
+ mods = []
+ for prop in properties:
+ # take the operation type from the property name
+ val = rawProperty(prop)
+ if str(prop).startswith('+'):
+ op = ldap.MOD_ADD
+ elif str(prop).startswith('-'):
+ op = ldap.MOD_DELETE
+ else:
+ op = ldap.MOD_REPLACE
+
+ mods.append((op, REPLICA_PROPNAME_TO_ATTRNAME[val], properties[prop]))
+
+ # that is fine now to apply the MOD
+ self.conn.modify_s(ents[0].dn, mods)
+
+
+ def getProperties(self, changelogdn=None, properties=None):
+ raise NotImplemented
\ No newline at end of file
diff --git a/src/lib389/lib389/properties.py b/src/lib389/lib389/properties.py
index 07b4d0d..4b5184a 100644
--- a/src/lib389/lib389/properties.py
+++ b/src/lib389/lib389/properties.py
@@ -4,6 +4,7 @@ Created on Dec 5, 2013
@author: tbordaz
'''
+
####################################
#
# Properties supported by the server
@@ -112,29 +113,140 @@ BACKEND_PROPNAME_TO_ATTRNAME = {BACKEND_SUFFIX:
'nsslapd-suffix',
####################################
#
+# Properties of a replica
+#
+####################################
+
+REPLICA_SUFFIX = 'suffix'
+REPLICA_ID = 'rid'
+REPLICA_TYPE = 'type'
+REPLICA_LEGACY_CONS = 'legacy'
+REPLICA_BINDDN = 'binddn'
+REPLICA_PURGE_INTERVAL = 'purge-interval'
+REPLICA_PURGE_DELAY = 'purge-delay'
+REPLICA_REFERRAL = 'referral'
+REPLICA_FLAGS = 'flags'
+
+REPLICA_OBJECTCLASS_VALUE = 'nsds5Replica'
+
+REPLICA_PROPNAME_TO_ATTRNAME = {
+ REPLICA_SUFFIX: 'nsds5replicaroot',
+ REPLICA_ID: 'nsds5replicaid',
+ REPLICA_TYPE: 'nsds5replicatype',
+ REPLICA_LEGACY_CONS:
'nsds5replicalegacyconsumer',
+ REPLICA_BINDDN: 'nsds5replicabinddn',
+ REPLICA_PURGE_INTERVAL:
'nsds5replicatombstonepurgeinterval',
+ REPLICA_PURGE_DELAY:
'nsds5ReplicaPurgeDelay',
+ REPLICA_REFERRAL:
'nsds5ReplicaReferral',
+ REPLICA_FLAGS: 'nsds5flags'}
+
+####################################
+#
+# Properties of a changelog
+#
+####################################
+CHANGELOG_NAME = 'cl-name'
+CHANGELOG_DIR = 'cl-dir'
+CHANGELOG_MAXAGE = 'cl-maxage'
+CHANGELOG_MAXENTRIES = 'cl-maxentries'
+CHANGELOG_TRIM_INTERVAL = 'cl-trim-interval'
+CHANGELOG_COMPACT_INTV = 'cl-compact-interval'
+
+CHANGELOG_PROPNAME_TO_ATTRNAME = {CHANGELOG_NAME: 'cn',
+ CHANGELOG_DIR:
'nsslapd-changelogdir',
+ CHANGELOG_MAXAGE:
'nsslapd-changelogmaxage',
+ CHANGELOG_MAXENTRIES:
'nsslapd-changelogmaxentries',
+ CHANGELOG_TRIM_INTERVAL:
'nsslapd-changelogtrim-interval',
+ CHANGELOG_COMPACT_INTV:
'nsslapd-changelogcompactdb-interval',}
+
+####################################
+#
+# Properties of an entry
+#
+####################################
+ENTRY_OBJECTCLASS = 'objectclass'
+ENTRY_SN = 'sn'
+ENTRY_CN = 'cn'
+ENTRY_TYPE_PERSON = 'person'
+ENTRY_TYPE_INETPERSON = 'inetperson'
+ENTRY_TYPE_GROUP = 'group'
+ENTRY_USERPASSWORD = 'userpassword'
+ENTRY_UID = 'uid'
+
+ENTRY_TYPE_TO_OBJECTCLASS = {ENTRY_TYPE_PERSON: ["top",
"person"],
+ ENTRY_TYPE_INETPERSON: ["top", "person",
"inetOrgPerson"]}
+
+####################################
+#
# Properties supported by the replica agreement
#
####################################
-RA_SCHEDULE_PROP='schedule'
-RA_TRANSPORT_PROT_PROP='transport-prot'
-RA_FRAC_EXCLUDE_PROP='fractional-exclude-attrs-inc'
-RA_FRAC_EXCLUDE_TOTAL_UPDATE_PROP='fractional-exclude-attrs-total'
-RA_FRAC_STRIP_PROP='fractional-strip-attrs'
-RA_CONSUMER_PORT='consumer-port'
-RA_CONSUMER_HOST='consumer-host'
-RA_CONSUMER_TOTAL_INIT='consumer-total-init'
-RA_TIMEOUT='timeout'
-
-RA_PROPNAME_TO_ATTRNAME = {RA_SCHEDULE_PROP:'nsds5replicaupdateschedule',
- RA_TRANSPORT_PROT_PROP:'nsds5replicatransportinfo',
- RA_FRAC_EXCLUDE_PROP:'nsDS5ReplicatedAttributeList',
-
RA_FRAC_EXCLUDE_TOTAL_UPDATE_PROP:'nsDS5ReplicatedAttributeListTotal',
- RA_FRAC_STRIP_PROP:'nsds5ReplicaStripAttrs',
- RA_CONSUMER_PORT:'nsds5replicaport',
- RA_CONSUMER_HOST:'nsds5ReplicaHost',
- RA_CONSUMER_TOTAL_INIT:'nsds5BeginReplicaRefresh',
- RA_TIMEOUT:'nsds5replicatimeout'}
-
-
-
\ No newline at end of file
+RA_NAME = 'name'
+RA_SUFFIX = 'suffix'
+RA_BINDDN = 'binddn'
+RA_BINDPW = 'bindpw'
+RA_METHOD = 'method'
+RA_DESCRIPTION = 'description'
+RA_SCHEDULE = 'schedule'
+RA_TRANSPORT_PROT = 'transport-prot'
+RA_FRAC_EXCLUDE = 'fractional-exclude-attrs-inc'
+RA_FRAC_EXCLUDE_TOTAL_UPDATE ='fractional-exclude-attrs-total'
+RA_FRAC_STRIP = 'fractional-strip-attrs'
+RA_CONSUMER_PORT = 'consumer-port'
+RA_CONSUMER_HOST = 'consumer-host'
+RA_CONSUMER_TOTAL_INIT = 'consumer-total-init'
+RA_TIMEOUT = 'timeout'
+RA_CHANGES = 'changes'
+
+RA_OBJECTCLASS_VALUE = "nsds5replicationagreement"
+
+RA_PROPNAME_TO_ATTRNAME = {RA_NAME: 'cn',
+ RA_SUFFIX: 'nsds5replicaroot',
+ RA_BINDDN: 'nsds5replicabinddn',
+ RA_BINDPW: 'nsds5replicacredentials',
+ RA_METHOD: 'nsds5replicabindmethod',
+ RA_DESCRIPTION: 'description',
+ RA_SCHEDULE:
'nsds5replicaupdateschedule',
+ RA_TRANSPORT_PROT: 'nsds5replicatransportinfo',
+ RA_FRAC_EXCLUDE:
'nsDS5ReplicatedAttributeList',
+
RA_FRAC_EXCLUDE_TOTAL_UPDATE:'nsDS5ReplicatedAttributeListTotal',
+ RA_FRAC_STRIP: 'nsds5ReplicaStripAttrs',
+ RA_CONSUMER_PORT: 'nsds5replicaport',
+ RA_CONSUMER_HOST: 'nsds5ReplicaHost',
+ RA_CONSUMER_TOTAL_INIT: 'nsds5BeginReplicaRefresh',
+ RA_TIMEOUT: 'nsds5replicatimeout',
+ RA_CHANGES:
'nsds5replicaChangesSentSinceStartup'}
+
+
+def rawProperty(prop):
+ '''
+ Return the string 'prop' without the heading '+'/'-'
+ @param prop - string of a property name
+
+ @return property name without heading '+'/'-'
+
+ @raise None
+ '''
+ if str(prop).startswith('+') or str(prop).startswith('-'):
+ return prop[1::]
+ else:
+ return prop
+
+
+def inProperties(prop, properties):
+ '''
+ Return True if 'prop' is in the 'properties' dictionary
+ Properties in 'properties' does NOT contain a heading
'+'/'-', but 'prop'
+ may contain heading '+'/'-'
+ @param prop - string of a property name
+ @param properties - dictionary of properties.
+
+ @return True/False
+
+ @raise None
+ '''
+ if rawProperty(prop) in properties:
+ return True
+ else:
+ return False
\ No newline at end of file
diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py
new file mode 100644
index 0000000..318ba6a
--- /dev/null
+++ b/src/lib389/lib389/replica.py
@@ -0,0 +1,553 @@
+'''
+Created on Dec 19, 2013
+
+@author: tbordaz
+'''
+
+import ldap
+import os
+import decimal
+import time
+from lib389 import DirSrv, Entry, NoSuchEntryError, InvalidArgumentError
+from lib389._constants import *
+from lib389._replication import RUV
+from lib389.properties import *
+from lib389.utils import normalizeDN, escapeDNValue
+
+
+
+
+
+class Replica(object):
+ proxied_methods = 'search_s getEntry'.split()
+
+ def __init__(self, conn):
+ """@param conn - a DirSrv instance"""
+ self.conn = conn
+ self.log = conn.log
+
+ def __getattr__(self, name):
+ if name in Replica.proxied_methods:
+ return DirSrv.__getattr__(self.conn, name)
+
+ def _get_mt_entry(self, suffix):
+ """Return the replica dn of the given suffix."""
+ mtent = self.conn.mappingtree.list(suffix=suffix)
+ return ','.join(("cn=replica", mtent.dn))
+
+ @staticmethod
+ def _valid_role(role):
+ if role != REPLICAROLE_MASTER and role != REPLICAROLE_HUB and role !=
REPLICAROLE_CONSUMER:
+ return False
+ else:
+ return True
+
+ @staticmethod
+ def _valid_rid(role, rid=None):
+ if role == REPLICAROLE_MASTER:
+ if not decimal.Decimal(rid) or (rid <= 0) or (rid
>=CONSUMER_REPLICAID):
+ return False
+ else:
+ if rid and (rid != CONSUMER_REPLICAID):
+ return False
+ return True
+
+ @staticmethod
+ def _set_or_default(attribute, properties, default):
+ '''
+ If 'attribute' or '+attribute' or '-attribute' exist
in 'properties'
+ it does nothing. Else it set (ldap.MOD_REPLACE) 'attribute' in
'properties'
+ to the 'default' value
+ '''
+ add_attribute = "+%s" % attribute
+ del_attribute = "-%s" % attribute
+ if attribute in properties or add_attribute in properties or del_attribute in
properties:
+ return
+ properties[attribute] = default
+
+ def create_repl_manager(self, repl_manager_dn=None, repl_manager_pw=None):
+ '''
+ Create an entry that will be used to bind as replica manager.
+
+ @param repl_manager_dn - DN of the bind entry. If not provided use the
default one
+ @param repl_manager_pw - Password of the entry. If not provide use the
default one
+
+ @return None
+
+ @raise - KeyError if can not find valid values of Bind DN and Pwd
+ '''
+
+ # check the DN and PW
+ try:
+ repl_manager_dn = repl_manager_dn or defaultProperties[REPLICATION_BIND_DN]
+ repl_manager_pw = repl_manager_pw or defaultProperties[REPLICATION_BIND_PW]
+ if not repl_manager_dn or not repl_manager_pw:
+ raise KeyError
+ except KeyError:
+ if not repl_manager_pw:
+ self.log.warning("replica_createReplMgr: bind DN password not
specified")
+ if not repl_manager_dn:
+ self.log.warning("replica_createReplMgr: bind DN not
specified")
+ raise
+
+ # if the replication manager entry already exists, ust return
+ try:
+ entries = self.search_s(repl_manager_dn, ldap.SCOPE_BASE,
"objectclass=*")
+ if entries:
+ #it already exist, fine
+ return
+ except ldap.NO_SUCH_OBJECT:
+ pass
+
+ # ok it does not exist, create it
+ try:
+ attrs = {
+ 'nsIdleTimeout': '0',
+ 'passwordExpirationTime': '20381010000000Z'
+ }
+ self.conn.setupBindDN(repl_manager_dn, repl_manager_pw, attrs)
+ except ldap.ALREADY_EXISTS:
+ self.log.warn("User already exists (weird we just checked: %s " %
repl_manager_dn)
+
+
+ def list(self, suffix=None, replica_dn=None):
+ """
+ Return a list of replica entries.
+ If 'replica_dn' is specified it returns the related entry.
+ If 'suffix' is specified it returns the replica that is configured
for that 'suffix'.
+ If both 'replica_dn' and 'suffix' are specified it returns
the 'replica_dn' entry.
+ If none of them are specified, it returns all the replicas
+
+ @param suffix - suffix of a replica
+ @param replica_dn - DN of a replica
+
+ @return replica entries
+
+ @raise search exceptions: ldap.NO_SUCH_OBJECT, ..
+ """
+
+ # set base/filtr according to the arguments
+ if replica_dn:
+ base = replica_dn
+ filtr = "(objectclass=%s)" % REPLICA_OBJECTCLASS_VALUE
+ elif suffix:
+ base = DN_MAPPING_TREE
+ filtr = "(&(objectclass=%s)(%s=%s))" %
(REPLICA_OBJECTCLASS_VALUE,
+
REPLICA_PROPNAME_TO_ATTRNAME[REPLICA_SUFFIX], suffix)
+ else:
+ base = DN_MAPPING_TREE
+ filtr = "(objectclass=%s)" % REPLICA_OBJECTCLASS_VALUE
+
+ # now do the effective search
+ ents = self.conn.search_s(base, ldap.SCOPE_SUBTREE, filtr)
+ return ents
+
+
+ def setProperties(self, suffix=None, replica_dn=None, replica_entry=None,
properties=None):
+ '''
+ Set the properties of the replica. If an 'replica_entry' (Entry) is
provided, it updates the entry, else
+ it updates the entry on the server. If the 'replica_dn' is provided
it retrieves the entry using it, else
+ it retrieve the replica using the 'suffix'.
+
+ @param suffix : suffix stored in that replica (online update)
+ @param replica_dn: DN of the replica (online update)
+ @param replica_entry: Entry of a replica (offline update)
+ @param properties: dictionary of properties
+ Supported properties are:
+ REPLICA_SUFFIX
+ REPLICA_ID
+ REPLICA_TYPE
+ REPLICA_LEGACY_CONS
+ REPLICA_BINDDN
+ REPLICA_PURGE_INTERVAL
+ REPLICA_PURGE_DELAY
+ REPLICA_REFERRAL
+ REPLICA_FLAGS
+
+ @return None
+
+ @raise ValueError: if unknown properties
+ ValueError: if invalid replica_entry
+ ValueError: if replica_dn or suffix are not associated to a replica
+ InvalidArgumentError: If missing mandatory parameter
+
+ '''
+
+ # No properties provided
+ if len(properties) == 0:
+ return
+
+ # check that the given properties are valid
+ for prop in properties:
+ # skip the prefix to add/del value
+ if not inProperties(prop, REPLICA_PROPNAME_TO_ATTRNAME):
+ raise ValueError("unknown property: %s" % prop)
+ else:
+ self.log.debug("setProperties: %s:%s" % (prop,
properties[prop]))
+
+ # At least we need to have suffix/replica_dn/replica_entry
+ if not suffix and not replica_dn and not replica_entry:
+ raise InvalidArgumentError("suffix and replica_dn and replica_entry are
missing")
+
+ # the caller provides a set of properties to set into a replica entry
+ if replica_entry:
+ if not isinstance(replica_entry, Entry):
+ raise ValueError("invalid instance of the replica_entry")
+
+ # that is fine, now set the values
+ for prop in properties:
+ val = rawProperty(prop)
+
+ # for Entry update it is a replace
+ replica_entry.update({REPLICA_PROPNAME_TO_ATTRNAME[val]:
properties[prop]})
+
+ return
+
+ # If it provides the suffix or the replicaDN, replica.list will
+ # return the appropriate entry
+ ents = self.conn.replica.list(suffix=suffix, replica_dn=replica_dn)
+ if len(ents) != 1:
+ if replica_dn:
+ raise ValueError("invalid replica DN: %s" % replica_dn)
+ else:
+ raise ValueError("invalid suffix: %s" % suffix)
+
+
+ # build the MODS
+ mods = []
+ for prop in properties:
+ # take the operation type from the property name
+ val = rawProperty(prop)
+ if str(prop).startswith('+'):
+ op = ldap.MOD_ADD
+ elif str(prop).startswith('-'):
+ op = ldap.MOD_DELETE
+ else:
+ op = ldap.MOD_REPLACE
+
+ mods.append((op, REPLICA_PROPNAME_TO_ATTRNAME[val], properties[prop]))
+
+ # that is fine now to apply the MOD
+ self.conn.modify_s(ents[0].dn, mods)
+
+
+ def getProperties(self, suffix=None, replica_dn=None, replica_entry=None,
properties=None):
+ raise NotImplementedError
+
+ def create(self, suffix=None, role=None, rid=None, args=None):
+ """
+ Create a replica entry on an existing suffix.
+
+ @param suffix - dn of suffix
+ @param role - REPLICAROLE_MASTER, REPLICAROLE_HUB or REPLICAROLE_CONSUMER
+ @param rid - number that identify the supplier replica
(role=REPLICAROLE_MASTER) in the topology.
+ For hub/consumer (role=REPLICAROLE_HUB or
REPLICAROLE_CONSUMER), rid value is not used.
+ This parameter is mandatory for supplier.
+
+ @param args - dictionnary of initial replica's properties
+ Supported properties are:
+ REPLICA_SUFFIX
+ REPLICA_ID
+ REPLICA_TYPE
+ REPLICA_LEGACY_CONS ['off']
+ REPLICA_BINDDN [defaultProperties[REPLICATION_BIND_DN]]
+ REPLICA_PURGE_INTERVAL
+ REPLICA_PURGE_DELAY
+ REPLICA_REFERRAL
+ REPLICA_FLAGS
+
+ @return replica DN
+
+ @raise InvalidArgumentError - if missing mandatory arguments
+ ValueError - argument with invalid value
+
+ """
+ # Check validity of role
+ if not role:
+ self.log.fatal("Replica.create: replica role not specify
(REPLICAROLE_*)")
+ raise InvalidArgumentError("role missing")
+
+ if not Replica._valid_role(role):
+ self.log.fatal("enableReplication: replica role invalid (%s) " %
role)
+ raise ValueError("invalid role: %s" % role)
+
+ # check the validity of 'rid'
+ if not Replica._valid_rid(role, rid=rid):
+ self.log.fatal("Replica.create: replica role is master but 'rid'
is missing or invalid value")
+ raise InvalidArgumentError("rid missing or invalid value")
+
+ # check the validity of the suffix
+ if not suffix:
+ self.log.fatal("Replica.create: suffix is missing")
+ raise InvalidArgumentError("suffix missing")
+ else:
+ nsuffix = normalizeDN(suffix)
+
+ # role is fine, set the replica type
+ if role == REPLICAROLE_MASTER:
+ rtype = REPLICA_RDWR_TYPE
+ else:
+ rtype = REPLICA_RDONLY_TYPE
+
+ # Set the properties provided as mandatory parameter
+ # The attribute name is not prefixed '+'/'-' =>
ldap.MOD_REPLACE
+ properties = {REPLICA_SUFFIX: nsuffix,
+ REPLICA_ID: str(rid),
+ REPLICA_TYPE: str(rtype)}
+
+ # If the properties in args are valid
+ # add them to the 'properties' dictionary
+ # The attribute name may be prefixed '+'/'-' => keep MOD type
as provided in args
+ if args:
+ for prop in args:
+ if not inProperties(prop, REPLICA_PROPNAME_TO_ATTRNAME):
+ raise ValueError("unknown property: %s" % prop)
+ properties[prop] = args[prop]
+
+ # Now set default values of unset properties
+ Replica._set_or_default(REPLICA_LEGACY_CONS, properties, 'off')
+ Replica._set_or_default(REPLICA_BINDDN, properties,
[defaultProperties[REPLICATION_BIND_DN]])
+
+ if role != REPLICAROLE_CONSUMER:
+ properties[REPLICA_FLAGS] = "1"
+
+ # create replica entry in mapping-tree
+ mtents = self.conn.mappingtree.list(suffix=nsuffix)
+ mtent = mtents[0]
+ dn_replica = ','.join((RDN_REPLICA, mtent.dn))
+ try:
+ entry = self.conn.getEntry(dn_replica, ldap.SCOPE_BASE)
+ self.log.warn("Already setup replica for suffix %r" % nsuffix)
+ self.conn.suffixes.setdefault(nsuffix, {})
+ self.conn.replica.setProperties(replica_dn=dn_replica,
properties=properties)
+ return dn_replica
+ except ldap.NO_SUCH_OBJECT:
+ entry = None
+
+ #
+ # Now create the replica entry
+ #
+ entry = Entry(dn_replica)
+ entry.setValues("objectclass", "top",
REPLICA_OBJECTCLASS_VALUE, "extensibleobject")
+ self.conn.replica.setProperties(replica_entry=entry, properties=properties)
+ self.conn.add_s(entry)
+
+ # check if the entry exists TODO better to raise!
+ self.conn._test_entry(dn_replica, ldap.SCOPE_BASE)
+
+ self.conn.suffixes[nsuffix] = {'dn': dn_replica, 'type': rtype}
+ return dn_replica
+
+ def delete(self, suffix=None):
+ '''
+ Delete a replica related to the provided suffix.
+ If this replica role was REPLICAROLE_HUB or REPLICAROLE_MASTER, it also
deletes the changelog
+ associated to that replica.
+ If it exists some replication agreement below that replica, they are
deleted.
+
+ @param suffix - dn of suffix
+
+ @return None
+
+ @raise ldap.NO_SUCH_OBJECT - if the suffix has no replica
+ InvalidArgumentError - if suffix is missing
+
+ '''
+ # check the validity of the suffix
+ if not suffix:
+ self.log.fatal("Replica.delete: suffix is missing")
+ raise InvalidArgumentError("suffix missing")
+ else:
+ nsuffix = normalizeDN(suffix)
+
+ # check the replica exists
+ mtents = self.conn.mappingtree.list(suffix=nsuffix)
+ mtent = mtents[0]
+ dn_replica = ','.join((RDN_REPLICA, mtent.dn))
+ try:
+ entry = self.conn.getEntry(dn_replica, ldap.SCOPE_BASE)
+ except ldap.NO_SUCH_OBJECT:
+ raise
+
+ raise NotImplementedError()
+
+ def enableReplication(self, suffix=None, role=None, replicaId=CONSUMER_REPLICAID,
binddn=None):
+ if not suffix:
+ self.log.fatal("enableReplication: suffix not specified")
+ raise ValueError("suffix missing")
+
+ if not role:
+ self.log.fatal("enableReplication: replica role not specify
(REPLICAROLE_*)")
+ raise ValueError("role missing")
+
+ #
+ # Check the validity of the parameters
+ #
+
+ # First role and replicaID
+ if role != REPLICAROLE_MASTER and role != REPLICAROLE_HUB and role !=
REPLICAROLE_CONSUMER:
+ self.log.fatal("enableReplication: replica role invalid (%s) " %
role)
+ raise ValueError("invalid role: %s" % role)
+
+ # master
+ replica_type = REPLICA_RDWR_TYPE
+ else:
+ # hub or consumer
+ replica_type = REPLICA_RDONLY_TYPE
+
+ if role == REPLICAROLE_MASTER:
+ # check the replicaId [1..CONSUMER_REPLICAID[
+ if not decimal.Decimal(replicaId) or (replicaId <= 0) or (replicaId
>=CONSUMER_REPLICAID):
+ self.log.fatal("enableReplication: invalid replicaId (%s) for a RW
replica" % replicaId)
+ raise ValueError("invalid replicaId %d (expected
[1..CONSUMER_REPLICAID[" % replicaId)
+ elif replicaId != CONSUMER_REPLICAID:
+ # check the replicaId is CONSUMER_REPLICAID
+ self.log.fatal("enableReplication: invalid replicaId (%s) for a Read
replica (expected %d)" % (replicaId, CONSUMER_REPLICAID))
+ raise ValueError("invalid replicaId: %d for HUB/CONSUMER replicaId is
CONSUMER_REPLICAID" % replicaId)
+
+ # Now check we have a suffix
+ entries_backend = self.conn.backend.list(suffix=suffix)
+ if not entries_backend:
+ self.log.fatal("enableReplication: enable to retrieve the backend for
%s" % suffix)
+ raise ValueError("no backend for suffix %s" % suffix)
+
+ ent = entries_backend[0]
+ if normalizeDN(suffix) != normalizeDN(ent.getValue('nsslapd-suffix')):
+ self.log.warning("enableReplication: suffix (%s) and backend suffix (%s)
differs" % (suffix, entries_backend[0].nsslapd-suffix))
+ pass
+
+ # Now prepare the bindDN property
+ if not binddn:
+ binddn = defaultProperties.get(REPLICATION_BIND_DN, None)
+ if not binddn:
+ # weird, internal error we do not retrieve the default replication bind
DN
+ # this replica will not be updatable through replication until the
binddn
+ # property will be set
+ self.log.warning("enableReplication: binddn not provided and default
value unavailable")
+ pass
+
+ # Now do the effectif job
+ # First add the changelog if master/hub
+ if (role == REPLICAROLE_MASTER) or (role == REPLICAROLE_HUB):
+ self.conn.changelog.create()
+
+ # Second create the default replica manager entry if it does not exist
+ # it should not be called from here but for the moment I am unsure when to create
it elsewhere
+ self.conn.replica.create_repl_manager()
+
+ # then enable replication
+ properties = {REPLICA_BINDDN: [binddn]}
+ ret = self.conn.replica.create(suffix=suffix, role=role, rid=replicaId,
args=properties)
+
+ return ret
+
+ def disableReplication(self, suffix=None):
+ if not suffix:
+ self.log.fatal("enableReplication: suffix not specified")
+ raise ValueError("suffix missing")
+
+ raise NotImplementedError
+
+ def check_init(self, agmtdn):
+ """returns tuple - first element is done/not done, 2nd is no
error/has error
+ @param agmtdn - the agreement dn
+ """
+ done, hasError = False, 0
+ attrlist = ['cn', 'nsds5BeginReplicaRefresh',
'nsds5replicaUpdateInProgress',
+ 'nsds5ReplicaLastInitStatus',
'nsds5ReplicaLastInitStart',
+ 'nsds5ReplicaLastInitEnd']
+ try:
+ entry = self.conn.getEntry(
+ agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist)
+ except NoSuchEntryError:
+ self.log.exception("Error reading status from agreement %r" %
agmtdn)
+ hasError = 1
+ else:
+ refresh = entry.nsds5BeginReplicaRefresh
+ inprogress = entry.nsds5replicaUpdateInProgress
+ status = entry.nsds5ReplicaLastInitStatus
+ if not refresh: # done - check status
+ if not status:
+ print "No status yet"
+ elif status.find("replica busy") > -1:
+ print "Update failed - replica busy - status", status
+ done = True
+ hasError = 2
+ elif status.find("Total update succeeded") > -1:
+ print "Update succeeded: status ", status
+ done = True
+ elif inprogress.lower() == 'true':
+ print "Update in progress yet not in progress: status ",
status
+ else:
+ print "Update failed: status", status
+ hasError = 1
+ done = True
+ elif self.verbose:
+ print "Update in progress: status", status
+
+ return done, hasError
+
+ def start_and_wait(self, agmtdn):
+ """@param agmtdn - agreement dn"""
+ rc = self.start_async(agmtdn)
+ if not rc:
+ rc = self.wait_init(agmtdn)
+ if rc == 2: # replica busy - retry
+ rc = self.start_and_wait(agmtdn)
+ return rc
+
+ def wait_init(self, agmtdn):
+ """Initialize replication and wait for completion.
+ @oaram agmtdn - agreement dn
+ """
+ done = False
+ haserror = 0
+ while not done and not haserror:
+ time.sleep(1) # give it a few seconds to get going
+ done, haserror = self.check_init(agmtdn)
+ return haserror
+
+ def start_async(self, agmtdn):
+ """Initialize replication without waiting.
+ @param agmtdn - agreement dn
+ """
+ self.log.info("Starting async replication %s" % agmtdn)
+ mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')]
+ self.conn.modify_s(agmtdn, mod)
+
+
+ def keep_in_sync(self, agmtdn):
+ """
+ @param agmtdn -
+ """
+ self.log.info("Setting agreement for continuous replication")
+ raise NotImplementedError("Check nsds5replicaupdateschedule before
writing!")
+
+ def ruv(self, suffix, tryrepl=False):
+ """return a replica update vector for the given suffix.
+
+ @param suffix - eg. 'o=netscapeRoot'
+
+ @raises NoSuchEntryError if missing
+ """
+ filt = "(&(nsUniqueID=%s)(objectclass=%s))" % (REPLICA_RUV_UUID,
REPLICA_OC_TOMBSTONE)
+ attrs = ['nsds50ruv', 'nsruvReplicaLastModified']
+ ents = self.conn.search_s(suffix, ldap.SCOPE_SUBTREE, filt, attrs)
+ ent = None
+ if ents and (len(ents) > 0):
+ ent = ents[0]
+ elif tryrepl:
+ self.log.warn("Could not get RUV from %r entry - trying cn=replica"
% suffix)
+ ensuffix = escapeDNValue(normalizeDN(suffix))
+ dn = ','.join(("cn=replica", "cn=%s" % ensuffix,
DN_MAPPING_TREE))
+ ents = self.conn.search_s(dn, ldap.SCOPE_BASE, "objectclass=*",
attrs)
+
+ if ents and (len(ents) > 0):
+ ent = ents[0]
+ self.log.debug("RUV entry is %r" % ent)
+ return RUV(ent)
+
+ raise NoSuchEntryError("RUV not found: suffix: %r" % suffix)
+
+
+
diff --git a/src/lib389/lib389/suffix.py b/src/lib389/lib389/suffix.py
index bd6d346..2e3f0aa 100644
--- a/src/lib389/lib389/suffix.py
+++ b/src/lib389/lib389/suffix.py
@@ -81,4 +81,12 @@ class Suffix(object):
raise NotImplemented
+ def setProperties(self, suffix):
+ '''
+ Supported properties:
+
+ '''
+
+ if not suffix:
+ raise ValueError("suffix is mandatory")
diff --git a/src/lib389/tests/agreement_test.py b/src/lib389/tests/agreement_test.py
new file mode 100644
index 0000000..ef878b5
--- /dev/null
+++ b/src/lib389/tests/agreement_test.py
@@ -0,0 +1,321 @@
+'''
+Created on Jan 9, 2014
+
+@author: tbordaz
+'''
+
+import ldap
+import time
+import sys
+import os
+
+from lib389 import InvalidArgumentError, NoSuchEntryError
+from lib389.agreement import Agreement
+from lib389._constants import *
+from lib389.properties import *
+from lib389 import DirSrv, Entry
+from _constants import REPLICAROLE_CONSUMER
+
+# Used for One master / One consumer topology
+HOST_MASTER = LOCALHOST
+PORT_MASTER = 40389
+SERVERID_MASTER = 'master'
+REPLICAID_MASTER = 1
+
+HOST_CONSUMER = LOCALHOST
+PORT_CONSUMER = 50389
+SERVERID_CONSUMER = 'consumer'
+
+TEST_REPL_DN = "uid=test,%s" % DEFAULT_SUFFIX
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'dirsrv'
+#INSTANCE_PREFIX = os.environ.get('PREFIX', None)
+INSTANCE_PREFIX = '/home/tbordaz/install'
+INSTANCE_BACKUP = os.environ.get('BACKUPDIR', DEFAULT_BACKUPDIR)
+
+SUFFIX = DEFAULT_SUFFIX
+ENTRY_DN = "cn=test_entry, %s" % SUFFIX
+
+
+
+class Test_Agreement():
+
+
+ def setUp(self):
+ #
+ # Master
+ #
+ # Create the master instance
+ master = DirSrv(verbose=False)
+ master.log.debug("Master allocated")
+ args = {SER_HOST: HOST_MASTER,
+ SER_PORT: PORT_MASTER,
+ SER_DEPLOYED_DIR: INSTANCE_PREFIX,
+ SER_SERVERID_PROP: SERVERID_MASTER
+ }
+ master.allocate(args)
+ if master.exists():
+ master.delete()
+ master.create()
+ master.open()
+
+ # enable replication
+ master.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_MASTER,
replicaId=REPLICAID_MASTER)
+ self.master = master
+
+
+ #
+ # Consumer
+ #
+ # Create the consumer instance
+ consumer = DirSrv(verbose=False)
+ consumer.log.debug("Consumer allocated")
+ args = {SER_HOST: HOST_CONSUMER,
+ SER_PORT: PORT_CONSUMER,
+ SER_DEPLOYED_DIR: INSTANCE_PREFIX,
+ SER_SERVERID_PROP: SERVERID_CONSUMER
+ }
+ consumer.allocate(args)
+ if consumer.exists():
+ consumer.delete()
+ consumer.create()
+ consumer.open()
+
+ # enable replication
+ consumer.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_CONSUMER)
+ self.consumer = consumer
+
+
+ def tearDown(self):
+ self.master.log.info("\n\n#########################\n###
TEARDOWN\n#########################\n")
+ for instance in (self.master, self.consumer):
+ if instance.exists():
+ instance.delete()
+
+ def test_create(self):
+ '''
+ Test to create a replica agreement and initialize the consumer.
+ Test on a unknown suffix
+ '''
+ self.master.log.info("\n\n#########################\n###
CREATE\n#########################\n")
+ properties = {RA_NAME: r'meTo_$host:$port',
+ RA_BINDDN: defaultProperties[REPLICATION_BIND_DN],
+ RA_BINDPW: defaultProperties[REPLICATION_BIND_PW],
+ RA_METHOD: defaultProperties[REPLICATION_BIND_METHOD],
+ RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
+ repl_agreement = self.master.agreement.create(suffix=SUFFIX,
host=self.consumer.host, port=self.consumer.port, properties=properties)
+ self.master.log.debug("%s created" % repl_agreement)
+ self.master.agreement.init(SUFFIX, HOST_CONSUMER, PORT_CONSUMER)
+ self.master.waitForReplInit(repl_agreement)
+
+ # Add a test entry
+ self.master.add_s(Entry((ENTRY_DN, {'objectclass': "top
person".split(),
+ 'sn': 'test_entry',
+ 'cn': 'test_entry'})))
+
+ # check replication is working
+ loop = 0
+ while loop <= 10:
+ try:
+ ent = self.consumer.getEntry(ENTRY_DN, ldap.SCOPE_BASE,
"(objectclass=*)")
+ break
+ except ldap.NO_SUCH_OBJECT:
+ time.sleep(1)
+ loop += 1
+ assert loop <= 10
+
+ # check that with an invalid suffix it raises NoSuchEntryError
+ try:
+ properties = {RA_NAME: r'meAgainTo_$host:$port'}
+ self.master.agreement.create(suffix="ou=dummy",
host=self.consumer.host, port=self.consumer.port, properties=properties)
+ except Exception as e:
+ self.master.log.info("Exception (expected): %s" %
type(e).__name__)
+ assert isinstance(e, NoSuchEntryError)
+
+ def test_list(self):
+ '''
+ List the replica agreement on a suffix => 1
+ Add a RA
+ List the replica agreements on that suffix again => 2
+ List a specific RA
+
+ PREREQUISITE: it exists a replica for SUFFIX and a replica agreement
+ '''
+ self.master.log.info("\n\n#########################\n###
LIST\n#########################\n")
+ ents = self.master.agreement.list(suffix=SUFFIX)
+ assert len(ents) == 1
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_HOST]) ==
self.consumer.host
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_PORT]) ==
str(self.consumer.port)
+
+ # Create a second RA to check .list returns 2 RA
+ properties = {RA_NAME: r'meTo_$host:$port',
+ RA_BINDDN: defaultProperties[REPLICATION_BIND_DN],
+ RA_BINDPW: defaultProperties[REPLICATION_BIND_PW],
+ RA_METHOD: defaultProperties[REPLICATION_BIND_METHOD],
+ RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
+ self.master.agreement.create(suffix=SUFFIX, host=self.consumer.host, port=12345,
properties=properties)
+ ents = self.master.agreement.list(suffix=SUFFIX)
+ assert len(ents) == 2
+
+ # Check we can .list a specific RA
+ ents = self.master.agreement.list(suffix=SUFFIX,
consumer_host=self.consumer.host, consumer_port=self.consumer.port)
+ assert len(ents) == 1
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_HOST]) ==
self.consumer.host
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_PORT]) ==
str(self.consumer.port)
+
+
+ def test_status(self):
+ self.master.log.info("\n\n#########################\n###
STATUS\n#########################")
+ ents = self.master.agreement.list(suffix=SUFFIX)
+ for ent in ents:
+ self.master.log.info("Status of %s: %s" % (ent.dn,
self.master.agreement.status(ent.dn)))
+
+ def test_schedule(self):
+
+ self.master.log.info("\n\n#########################\n###
SCHEDULE\n#########################")
+ ents = self.master.agreement.list(suffix=SUFFIX,
consumer_host=self.consumer.host, consumer_port=self.consumer.port)
+ assert len(ents) == 1
+
+ self.master.agreement.schedule(ents[0].dn, Agreement.ALWAYS)
+ ents = self.master.agreement.list(suffix=SUFFIX,
consumer_host=self.consumer.host, consumer_port=self.consumer.port)
+ assert len(ents) == 1
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_SCHEDULE]) ==
Agreement.ALWAYS
+
+ self.master.agreement.schedule(ents[0].dn, Agreement.NEVER)
+ ents = self.master.agreement.list(suffix=SUFFIX,
consumer_host=self.consumer.host, consumer_port=self.consumer.port)
+ assert len(ents) == 1
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_SCHEDULE]) == Agreement.NEVER
+
+ CUSTOM_SCHEDULE="0000-1234 6420"
+ self.master.agreement.schedule(ents[0].dn, CUSTOM_SCHEDULE)
+ ents = self.master.agreement.list(suffix=SUFFIX,
consumer_host=self.consumer.host, consumer_port=self.consumer.port)
+ assert len(ents) == 1
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_SCHEDULE]) == CUSTOM_SCHEDULE
+
+ # check that with an invalid HOUR schedule raise ValueError
+ try:
+ CUSTOM_SCHEDULE="2500-1234 6420"
+ self.master.agreement.schedule(ents[0].dn, CUSTOM_SCHEDULE)
+ except Exception as e:
+ self.master.log.info("Exception (expected) HOUR: %s" %
type(e).__name__)
+ assert isinstance(e, ValueError)
+
+ try:
+ CUSTOM_SCHEDULE="0000-2534 6420"
+ self.master.agreement.schedule(ents[0].dn, CUSTOM_SCHEDULE)
+ except Exception as e:
+ self.master.log.info("Exception (expected) HOUR: %s" %
type(e).__name__)
+ assert isinstance(e, ValueError)
+
+ # check that with an starting HOUR after ending HOUR raise ValueError
+ try:
+ CUSTOM_SCHEDULE="1300-1234 6420"
+ self.master.agreement.schedule(ents[0].dn, CUSTOM_SCHEDULE)
+ except Exception as e:
+ self.master.log.info("Exception (expected) HOUR: %s" %
type(e).__name__)
+ assert isinstance(e, ValueError)
+
+ # check that with an invalid MIN schedule raise ValueError
+ try:
+ CUSTOM_SCHEDULE="0062-1234 6420"
+ self.master.agreement.schedule(ents[0].dn, CUSTOM_SCHEDULE)
+ except Exception as e:
+ self.master.log.info("Exception (expected) MIN: %s" %
type(e).__name__)
+ assert isinstance(e, ValueError)
+
+ try:
+ CUSTOM_SCHEDULE="0000-1362 6420"
+ self.master.agreement.schedule(ents[0].dn, CUSTOM_SCHEDULE)
+ except Exception as e:
+ self.master.log.info("Exception (expected) MIN: %s" %
type(e).__name__)
+ assert isinstance(e, ValueError)
+
+ # check that with an invalid DAYS schedule raise ValueError
+ try:
+ CUSTOM_SCHEDULE="0000-1234 6-420"
+ self.master.agreement.schedule(ents[0].dn, CUSTOM_SCHEDULE)
+ except Exception as e:
+ self.master.log.info("Exception (expected) MIN: %s" %
type(e).__name__)
+ assert isinstance(e, ValueError)
+
+ try:
+ CUSTOM_SCHEDULE="0000-1362 64209"
+ self.master.agreement.schedule(ents[0].dn, CUSTOM_SCHEDULE)
+ except Exception as e:
+ self.master.log.info("Exception (expected) MIN: %s" %
type(e).__name__)
+ assert isinstance(e, ValueError)
+
+ try:
+ CUSTOM_SCHEDULE="0000-1362 01234560"
+ self.master.agreement.schedule(ents[0].dn, CUSTOM_SCHEDULE)
+ except Exception as e:
+ self.master.log.info("Exception (expected) MIN: %s" %
type(e).__name__)
+ assert isinstance(e, ValueError)
+
+ def test_getProperties(self):
+ self.master.log.info("\n\n#########################\n###
GETPROPERTIES\n#########################")
+ ents = self.master.agreement.list(suffix=SUFFIX,
consumer_host=self.consumer.host, consumer_port=self.consumer.port)
+ assert len(ents) == 1
+ properties = self.master.agreement.getProperties(agmnt_dn=ents[0].dn)
+ for prop in properties:
+ self.master.log.info("RA %s : %s -> %s" % (prop,
RA_PROPNAME_TO_ATTRNAME[prop], properties[prop]))
+
+ properties = self.master.agreement.getProperties(agmnt_dn=ents[0].dn,
properties=[RA_BINDDN])
+ assert len(properties) == 1
+ for prop in properties:
+ self.master.log.info("RA %s : %s -> %s" % (prop,
RA_PROPNAME_TO_ATTRNAME[prop], properties[prop]))
+
+ def test_setProperties(self):
+ self.master.log.info("\n\n#########################\n###
SETPROPERTIES\n#########################")
+ ents = self.master.agreement.list(suffix=SUFFIX,
consumer_host=self.consumer.host, consumer_port=self.consumer.port)
+ assert len(ents) == 1
+ test_schedule = "1234-2345 12345"
+ test_desc = "hello world !"
+ self.master.agreement.setProperties(agmnt_dn=ents[0].dn, properties={RA_SCHEDULE:
test_schedule, RA_DESCRIPTION: test_desc})
+ properties = self.master.agreement.getProperties(agmnt_dn=ents[0].dn,
properties=[RA_SCHEDULE, RA_DESCRIPTION])
+ assert len(properties) == 2
+ assert properties[RA_SCHEDULE][0] == test_schedule
+ assert properties[RA_DESCRIPTION][0] == test_desc
+
+ def test_changes(self):
+ self.master.log.info("\n\n#########################\n###
CHANGES\n#########################")
+ ents = self.master.agreement.list(suffix=SUFFIX,
consumer_host=self.consumer.host, consumer_port=self.consumer.port)
+ assert len(ents) == 1
+ value = self.master.agreement.changes(agmnt_dn=ents[0].dn)
+ self.master.log.info("\ntest_changes: %d changes\n" % value)
+ assert value > 0
+
+ # do an update
+ TEST_STRING = 'hello you'
+ mod = [(ldap.MOD_REPLACE, 'description', [TEST_STRING])]
+ self.master.modify_s(ENTRY_DN, mod)
+
+ # the update has been replicated
+ loop = 0
+ while loop <= 10:
+ ent = self.consumer.getEntry(ENTRY_DN, ldap.SCOPE_BASE,
"(objectclass=*)")
+ if ent and ent.hasValue('description'):
+ if ent.getValue('description') == TEST_STRING:
+ break
+ time.sleep(1)
+ loop += 1
+ assert loop <= 10
+
+ # check change number
+ newvalue = self.master.agreement.changes(agmnt_dn=ents[0].dn)
+ self.master.log.info("\ntest_changes: %d changes\n" % newvalue)
+ assert (value + 1) == newvalue
+
+if __name__ == "__main__":
+ test = Test_Agreement()
+ test.setUp()
+
+ test.test_create()
+ test.test_list()
+ test.test_status()
+ test.test_schedule()
+ test.test_getProperties()
+ test.test_setProperties()
+ test.test_changes()
+
+ test.tearDown()
\ No newline at end of file
diff --git a/src/lib389/tests/replica_test.py b/src/lib389/tests/replica_test.py
index c358296..a006a21 100644
--- a/src/lib389/tests/replica_test.py
+++ b/src/lib389/tests/replica_test.py
@@ -1,326 +1,625 @@
-"""Brooker classes to organize ldap methods.
- Stuff is split in classes, like:
- * Replica
- * Backend
- * Suffix
+'''
+Created on Jan 08, 2014
- You will access this from:
- DirSrv.backend.methodName()
-"""
+@author: tbordaz
+'''
-from nose import *
-from nose.tools import *
-
-import config
-from config import log
-from config import *
import ldap
import time
import sys
-import lib389
-from lib389 import DirSrv, Entry
-from lib389 import NoSuchEntryError
-from lib389 import utils
-from lib389.tools import DirSrvTools
-from subprocess import Popen
-from random import randint
-from lib389.brooker import Replica
-from lib389 import MASTER_TYPE, DN_MAPPING_TREE, DN_CHANGELOG
-# Test harnesses
-from dsadmin_test import drop_backend, addbackend_harn
-from dsadmin_test import drop_added_entries
-
-conn = None
-added_entries = None
-added_backends = None
-
-MOCK_REPLICA_ID = '12'
-
-
-def setup():
- # uses an existing 389 instance
- # add a suffix
- # add an agreement
- # This setup is quite verbose but to test DirSrv method we should
- # do things manually. A better solution would be to use an LDIF.
- global conn
- conn = DirSrv(**config.auth)
- conn.verbose = True
- conn.added_entries = []
- conn.added_backends = set(['o=mockbe1'])
- conn.added_replicas = []
-
- # add a backend for testing ruv and agreements
- suffix = 'o=testReplica'
- backend = 'testReplicadb'
- backendEntry, dummy = conn.backend.add(suffix, benamebase=backend)
- suffixEntry = conn.backend.setup_mt(suffix, backend)
-
- # add another backend for testing replica.add()
- suffix = 'o=testReplicaCreation'
- backend = 'testReplicaCreationdb'
- backendEntry, dummy = conn.backend.add(suffix, benamebase=backend)
- suffixEntry = conn.backend.setup_mt(suffix, backend)
-
- # replication needs changelog
- conn.replica.changelog()
- # add rmanager entry
- try:
- conn.add_s(Entry((DN_RMANAGER, {
- 'objectclass': "top person inetOrgPerson".split(),
- 'sn': ["bind dn pseudo user"],
- 'cn': 'replication manager',
- 'uid': 'rmanager'
- }))
- )
- conn.added_entries.append(DN_RMANAGER)
- except ldap.ALREADY_EXISTS:
- pass
+import os
- # add a master replica entry
- # to test ruv and agreements
- replica_dn = ','.join(
- ['cn=replica', 'cn="o=testReplica"', DN_MAPPING_TREE])
- replica_e = Entry(replica_dn)
- replica_e.update({
- 'objectclass': ["top", "nsds5replica",
"extensibleobject"],
- 'cn': "replica",
- 'nsds5replicaroot': 'o=testReplica',
- 'nsds5replicaid': MOCK_REPLICA_ID,
- 'nsds5replicatype': '3',
- 'nsds5flags': '1',
- 'nsds5replicabinddn': DN_RMANAGER
- })
- try:
- conn.add_s(replica_e)
- except ldap.ALREADY_EXISTS:
- pass
- conn.added_entries.append(replica_dn)
-
- agreement_dn = ','.join(('cn=testAgreement', replica_dn))
- agreement_e = Entry(agreement_dn)
- agreement_e.update({
- 'objectclass': ["top",
"nsds5replicationagreement"],
- 'cn': 'testAgreement',
- 'nsds5replicahost': 'localhost',
- 'nsds5replicaport': '22389',
- 'nsds5replicatimeout': '120',
- 'nsds5replicabinddn': DN_RMANAGER,
- 'nsds5replicacredentials': 'password',
- 'nsds5replicabindmethod': 'simple',
- 'nsds5replicaroot': 'o=testReplica',
- 'nsds5replicaupdateschedule': '0000-2359
0123456',
- 'description': 'testAgreement'
- })
- try:
- conn.add_s(agreement_e)
- except ldap.ALREADY_EXISTS:
- pass
- conn.added_entries.append(agreement_dn)
- conn.agreement_dn = agreement_dn
-
-
-def teardown():
- global conn
- drop_added_entries(conn)
- conn.delete_s(','.join(['cn="o=testreplica"',
DN_MAPPING_TREE]))
- conn.backend.delete('testReplicadb')
+from lib389 import InvalidArgumentError
+from lib389._constants import *
+from lib389.properties import *
+from lib389 import DirSrv, Entry
+from _constants import REPLICAROLE_CONSUMER
+
+# Used for One master / One consumer topology
+HOST_MASTER = LOCALHOST
+PORT_MASTER = 40389
+SERVERID_MASTER = 'master'
+REPLICAID_MASTER = 1
+
+HOST_CONSUMER = LOCALHOST
+PORT_CONSUMER = 50389
+SERVERID_CONSUMER = 'consumer'
+
+TEST_REPL_DN = "uid=test,%s" % DEFAULT_SUFFIX
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'dirsrv'
+#INSTANCE_PREFIX = os.environ.get('PREFIX', None)
+INSTANCE_PREFIX = '/home/tbordaz/install'
+INSTANCE_BACKUP = os.environ.get('BACKUPDIR', DEFAULT_BACKUPDIR)
+NEW_SUFFIX_1 = 'ou=test_master'
+NEW_BACKEND_1 = 'test_masterdb'
+NEW_RM_1 = "cn=replrepl,%s" % NEW_SUFFIX_1
+
+NEW_SUFFIX_2 = 'ou=test_consumer'
+NEW_BACKEND_2 = 'test_consumerdb'
+
+NEW_SUFFIX_3 = 'ou=test_enablereplication_1'
+NEW_BACKEND_3 = 'test_enablereplicationdb_1'
+
+NEW_SUFFIX_4 = 'ou=test_enablereplication_2'
+NEW_BACKEND_4 = 'test_enablereplicationdb_2'
+
+NEW_SUFFIX_5 = 'ou=test_enablereplication_3'
+NEW_BACKEND_5 = 'test_enablereplicationdb_3'
+
+
+class Test_replica():
+
+
+ def setUp(self):
+ # Create the master instance
+ master = DirSrv(verbose=False)
+ master.log.debug("Master allocated")
+ args = {SER_HOST: HOST_MASTER,
+ SER_PORT: PORT_MASTER,
+ SER_DEPLOYED_DIR: INSTANCE_PREFIX,
+ SER_SERVERID_PROP: SERVERID_MASTER
+ }
+ master.allocate(args)
+ if master.exists():
+ master.delete()
+ master.create()
+ master.open()
+ self.master = master
+
+ # Create the consumer instance
+ consumer = DirSrv(verbose=False)
+ consumer.log.debug("Consumer allocated")
+ args = {SER_HOST: HOST_CONSUMER,
+ SER_PORT: PORT_CONSUMER,
+ SER_DEPLOYED_DIR: INSTANCE_PREFIX,
+ SER_SERVERID_PROP: SERVERID_CONSUMER
+ }
+ consumer.allocate(args)
+ if consumer.exists():
+ consumer.delete()
+ consumer.create()
+ consumer.open()
+ self.consumer = consumer
+
+
+ def tearDown(self):
+ self.master.log.info("\n\n#########################\n###
TEARDOWN\n#########################")
+ for instance in (self.master, self.consumer):
+ if instance.exists():
+ instance.delete()
+
+ def test_create(self):
+ self.master.log.info("\n\n#########################\n###
CREATE\n#########################")
+ '''
+ This test creates
+ - suffix/backend (NEW_SUFFIX_[12], NEW_BACKEND_[12]) : Master
+ - suffix/backend (NEW_SUFFIX_[12], NEW_BACKEND_[12]) : Consumer
+ - replica NEW_SUFFIX_1 as MASTER : Master
+ - replica NEW_SUFFIX_2 as CONSUMER : Master
+ '''
+ #
+ # MASTER (suffix/backend)
+ #
+ backendEntry = self.master.backend.create(suffix=NEW_SUFFIX_1,
properties={BACKEND_NAME: NEW_BACKEND_1})
+ backendEntry = self.master.backend.create(suffix=NEW_SUFFIX_2,
properties={BACKEND_NAME: NEW_BACKEND_2})
+
+ ents = self.master.mappingtree.list()
+ master_nb_mappingtree = len(ents)
+
+ # create a first additional mapping tree
+ self.master.mappingtree.create(NEW_SUFFIX_1, bename=NEW_BACKEND_1)
+ ents = self.master.mappingtree.list()
+ assert len(ents) == (master_nb_mappingtree + 1)
+ self.master.add_s(Entry((NEW_SUFFIX_1, {'objectclass': "top
organizationalunit".split(),
+ 'ou':
NEW_SUFFIX_1.split('=',1)[1]})))
+
+
+ # create a second additional mapping tree
+ self.master.mappingtree.create(NEW_SUFFIX_2, bename=NEW_BACKEND_2)
+ ents = self.master.mappingtree.list()
+ assert len(ents) == (master_nb_mappingtree + 2)
+ self.master.add_s(Entry((NEW_SUFFIX_2, {'objectclass': "top
organizationalunit".split(),
+ 'ou':
NEW_SUFFIX_2.split('=',1)[1]})))
+ self.master.log.info('test_create): Master it exists now %d suffix(es)' %
len(ents))
+
+ #
+ # CONSUMER (suffix/backend)
+ #
+ backendEntry = self.consumer.backend.create(suffix=NEW_SUFFIX_1,
properties={BACKEND_NAME: NEW_BACKEND_1})
+ backendEntry = self.consumer.backend.create(suffix=NEW_SUFFIX_2,
properties={BACKEND_NAME: NEW_BACKEND_2})
+
+ ents = self.consumer.mappingtree.list()
+ consumer_nb_mappingtree = len(ents)
+
+ # create a first additional mapping tree
+ self.consumer.mappingtree.create(NEW_SUFFIX_1, bename=NEW_BACKEND_1)
+ ents = self.consumer.mappingtree.list()
+ assert len(ents) == (consumer_nb_mappingtree + 1)
+ self.consumer.add_s(Entry((NEW_SUFFIX_1, {'objectclass': "top
organizationalunit".split(),
+ 'ou':
NEW_SUFFIX_1.split('=',1)[1]})))
+
+ # create a second additional mapping tree
+ self.consumer.mappingtree.create(NEW_SUFFIX_2, bename=NEW_BACKEND_2)
+ ents = self.consumer.mappingtree.list()
+ assert len(ents) == (consumer_nb_mappingtree + 2)
+ self.consumer.add_s(Entry((NEW_SUFFIX_2, {'objectclass': "top
organizationalunit".split(),
+ 'ou':
NEW_SUFFIX_2.split('=',1)[1]})))
+ self.consumer.log.info('test_create): Consumer it exists now %d
suffix(es)' % len(ents))
+
+ #
+ # Now create REPLICAS on master
+ #
+ # check it exists this entry to stores the changelogs
+ self.master.changelog.create()
+
+ # create a master
+ self.master.replica.create(suffix=NEW_SUFFIX_1, role=REPLICAROLE_MASTER, rid=1)
+ ents = self.master.replica.list()
+ assert len(ents) == 1
+ self.master.log.info('test_create): Master replica %s' % ents[0].dn)
+
+ # create a consumer
+ self.master.replica.create(suffix=NEW_SUFFIX_2, role=REPLICAROLE_CONSUMER)
+ ents = self.master.replica.list()
+ assert len(ents) == 2
+ ents = self.master.replica.list(suffix=NEW_SUFFIX_2)
+ self.master.log.info('test_create): Consumer replica %s' % ents[0].dn)
+
+ #
+ # Now create REPLICAS on consumer
+ #
+ # create a master
+ self.consumer.replica.create(suffix=NEW_SUFFIX_1, role=REPLICAROLE_CONSUMER)
+ ents = self.consumer.replica.list()
+ assert len(ents) == 1
+ self.consumer.log.info('test_create): Consumer replica %s' % ents[0].dn)
+
+ # create a consumer
+ self.consumer.replica.create(suffix=NEW_SUFFIX_2, role=REPLICAROLE_CONSUMER)
+ ents = self.consumer.replica.list()
+ assert len(ents) == 2
+ ents = self.consumer.replica.list(suffix=NEW_SUFFIX_2)
+ self.consumer.log.info('test_create): Consumer replica %s' % ents[0].dn)
+
+ def test_list(self):
+ self.master.log.info("\n\n#########################\n###
LIST\n#########################")
+ '''
+ This test checks:
+ - existing replicas can be retrieved
+ - access to unknown replica does not fail
+
+ PRE-CONDITION:
+ It exists on MASTER two replicas NEW_SUFFIX_1 and NEW_SUFFIX_2
+ created by test_create()
+ '''
+ ents = self.master.replica.list()
+ assert len(ents) == 2
+
+ # Check we can retrieve a replica with its suffix
+ ents = self.master.replica.list(suffix=NEW_SUFFIX_1)
+ assert len(ents) == 1
+ replica_dn_1 = ents[0].dn
+
+ # Check we can retrieve a replica with its suffix
+ ents = self.master.replica.list(suffix=NEW_SUFFIX_2)
+ assert len(ents) == 1
+ replica_dn_2 = ents[0].dn
+
+ # Check we can retrieve a replica with its DN
+ ents = self.master.replica.list(replica_dn=replica_dn_1)
+ assert len(ents) == 1
+ assert replica_dn_1 == ents[0].dn
+
+
+ # Check we can retrieve a replica if we provide DN and suffix
+ ents = self.master.replica.list(suffix=NEW_SUFFIX_2, replica_dn=replica_dn_2)
+ assert len(ents) == 1
+ assert replica_dn_2 == ents[0].dn
+
+ # Check DN is used before suffix name
+ ents = self.master.replica.list(suffix=NEW_SUFFIX_2, replica_dn=replica_dn_1)
+ assert len(ents) == 1
+ assert replica_dn_1 == ents[0].dn
+
+ # Check that invalid value does not break
+ ents = self.master.replica.list(suffix="X")
+ for ent in ents:
+ self.master.log.critical("Unexpected replica: %s" % ent.dn)
+ assert len(ents) == 0
+
+ def test_create_repl_manager(self):
+ self.master.log.info("\n\n#########################\n###
CREATE_REPL_MANAGER\n#########################")
+ '''
+ The tests are
+ - create the default Replication manager/Password
+ - create a specific Replication manager/ default Password
+ - Check we can bind successfully
+ - create a specific Replication manager / specific Password
+ - Check we can bind successfully
+ '''
+ # First create the default replication manager
+ self.consumer.replica.create_repl_manager()
+ ents = self.consumer.search_s(defaultProperties[REPLICATION_BIND_DN],
ldap.SCOPE_BASE, "objectclass=*")
+ assert len(ents) == 1
+ assert ents[0].dn == defaultProperties[REPLICATION_BIND_DN]
+
+ # Second create a custom replication manager under NEW_SUFFIX_2
+ rm_dn = "cn=replrepl,%s" % NEW_SUFFIX_2
+ self.consumer.replica.create_repl_manager(repl_manager_dn=rm_dn)
+ ents = self.consumer.search_s(rm_dn, ldap.SCOPE_BASE, "objectclass=*")
+ assert len(ents) == 1
+ assert ents[0].dn == rm_dn
+
+ # Check we can bind
+ self.consumer.simple_bind_s(rm_dn, defaultProperties[REPLICATION_BIND_PW])
+
+ # Check we fail to bind
+ try:
+ self.consumer.simple_bind_s(rm_dn, "dummy")
+ except Exception as e:
+ self.consumer.log.info("Exception: %s" % type(e).__name__)
+ assert isinstance(e, ldap.INVALID_CREDENTIALS)
+
+ #now rebind
+ self.consumer.simple_bind_s(self.consumer.binddn, self.consumer.bindpw)
+
+
+ # Create a custom replication manager under NEW_SUFFIX_1 with a specified
password
+ rm_dn = NEW_RM_1
+ self.consumer.replica.create_repl_manager(repl_manager_dn=rm_dn,
repl_manager_pw="Secret123")
+ ents = self.consumer.search_s(rm_dn, ldap.SCOPE_BASE, "objectclass=*")
+ assert len(ents) == 1
+ assert ents[0].dn == rm_dn
+
+ # Check we can bind
+ self.consumer.simple_bind_s(rm_dn, "Secret123")
+
+ # Check we fail to bind
+ try:
+ self.consumer.simple_bind_s(rm_dn, "dummy")
+ except Exception as e:
+ self.consumer.log.info("Exception: %s" % type(e).__name__)
+ assert isinstance(e, ldap.INVALID_CREDENTIALS)
+ #now rebind
+ self.consumer.simple_bind_s(self.consumer.binddn, self.consumer.bindpw)
+
+ def test_enableReplication(self):
+ self.master.log.info("\n\n#########################\n###
ENABLEREPLICATION\n#########################")
+ '''
+ It checks
+ - Ability to enable replication on a supplier
+ - Ability to enable replication on a consumer
+ - Failure to enable replication with wrong replicaID on supplier
+ - Failure to enable replication with wrong replicaID on consumer
+ '''
+ #
+ # MASTER (suffix/backend)
+ #
+ backendEntry = self.master.backend.create(suffix=NEW_SUFFIX_3,
properties={BACKEND_NAME: NEW_BACKEND_3})
+
+ ents = self.master.mappingtree.list()
+ master_nb_mappingtree = len(ents)
+
+ # create a first additional mapping tree
+ self.master.mappingtree.create(NEW_SUFFIX_3, bename=NEW_BACKEND_3)
+ ents = self.master.mappingtree.list()
+ assert len(ents) == (master_nb_mappingtree + 1)
+ self.master.add_s(Entry((NEW_SUFFIX_3, {'objectclass': "top
organizationalunit".split(),
+ 'ou':
NEW_SUFFIX_3.split('=',1)[1]})))
+
+ try:
+ # a supplier should have replicaId in [1..CONSUMER_REPLICAID[
+ self.master.replica.enableReplication(suffix=NEW_SUFFIX_3,
role=REPLICAROLE_MASTER, replicaId=CONSUMER_REPLICAID)
+ except Exception as e:
+ self.master.log.info("Exception (expected): %s" %
type(e).__name__)
+ assert isinstance(e, ValueError)
+ self.master.replica.enableReplication(suffix=NEW_SUFFIX_3,
role=REPLICAROLE_MASTER, replicaId=1)
+
+ #
+ # MASTER (suffix/backend)
+ #
+ backendEntry = self.master.backend.create(suffix=NEW_SUFFIX_4,
properties={BACKEND_NAME: NEW_BACKEND_4})
+
+ ents = self.master.mappingtree.list()
+ master_nb_mappingtree = len(ents)
+
+ # create a first additional mapping tree
+ self.master.mappingtree.create(NEW_SUFFIX_4, bename=NEW_BACKEND_4)
+ ents = self.master.mappingtree.list()
+ assert len(ents) == (master_nb_mappingtree + 1)
+ self.master.add_s(Entry((NEW_SUFFIX_4, {'objectclass': "top
organizationalunit".split(),
+ 'ou':
NEW_SUFFIX_4.split('=',1)[1]})))
+ try:
+ # A consumer should have CONSUMER_REPLICAID not '1'
+ self.master.replica.enableReplication(suffix=NEW_SUFFIX_4,
role=REPLICAROLE_CONSUMER, replicaId=1)
+ except Exception as e:
+ self.master.log.info("Exception (expected): %s" %
type(e).__name__)
+ assert isinstance(e, ValueError)
+ self.master.replica.enableReplication(suffix=NEW_SUFFIX_4,
role=REPLICAROLE_CONSUMER)
+
+ def test_disableReplication(self):
+ self.master.log.info("\n\n#########################\n###
DISABLEREPLICATION\n#########################")
+ '''
+ Currently not implemented
+ '''
+ try:
+ self.master.replica.disableReplication(suffix=NEW_SUFFIX_4)
+ except Exception as e:
+ self.master.log.info("Exception (expected): %s" %
type(e).__name__)
+ assert isinstance(e, NotImplementedError)
+
+ def test_setProperties(self):
+ self.master.log.info("\n\n#########################\n###
SETPROPERTIES\n#########################")
+ '''
+ Set some properties
+ Verified that valid properties are set
+ Verified that invalid properties raise an Exception
+
+ PRE-REQUISITE: it exists a replica for NEW_SUFFIX_1
+ '''
+
+ # set valid values to SUFFIX_1
+ properties = {REPLICA_LEGACY_CONS: 'off',
+ REPLICA_BINDDN: NEW_RM_1,
+ REPLICA_PURGE_INTERVAL: str(3600),
+ REPLICA_PURGE_DELAY: str(5*24*3600),
+ REPLICA_REFERRAL: "ldap://%s:1234/" % LOCALHOST}
+ self.master.replica.setProperties(suffix=NEW_SUFFIX_1, properties=properties)
+
+ # Check the values have been written
+ replicas = self.master.replica.list(suffix=NEW_SUFFIX_1)
+ assert len(replicas) == 1
+ for prop in properties:
+ attr = REPLICA_PROPNAME_TO_ATTRNAME[prop]
+ val = replicas[0].getValue(attr)
+ self.master.log.info("Replica[%s] -> %s: %s" % (prop, attr,
val))
+ assert val == properties[prop]
+
+ # Check invalid properties raise exception
+ try:
+ properties = {"dummy": 'dummy'}
+ self.master.replica.setProperties(suffix=NEW_SUFFIX_1,
properties=properties)
+ except Exception as e:
+ self.master.log.info("Exception (expected): %s" %
type(e).__name__)
+ assert isinstance(e, ValueError)
+
+ # check call without suffix/dn/entry raise InvalidArgumentError
+ try:
+ properties = {REPLICA_LEGACY_CONS: 'off'}
+ self.master.replica.setProperties(properties=properties)
+ except Exception as e:
+ self.master.log.info("Exception (expected): %s" %
type(e).__name__)
+ assert isinstance(e, InvalidArgumentError)
+
+ # check that if we do not provide a valid entry it raises ValueError
+ try:
+ properties = {REPLICA_LEGACY_CONS: 'off'}
+ self.master.replica.setProperties(replica_entry="dummy",
properties=properties)
+ except Exception as e:
+ self.master.log.info("Exception (expected): %s" %
type(e).__name__)
+ assert isinstance(e, ValueError)
+
+ # check that with an invalid suffix or replica_dn it raise ValueError
+ try:
+ properties = {REPLICA_LEGACY_CONS: 'off'}
+ self.master.replica.setProperties(suffix="dummy",
properties=properties)
+ except Exception as e:
+ self.master.log.info("Exception (expected): %s" %
type(e).__name__)
+ assert isinstance(e, ValueError)
+
+ def test_getProperties(self):
+ self.master.log.info("\n\n#########################\n###
GETPROPERTIES\n#########################")
+ '''
+ Currently not implemented
+ '''
+ try:
+ properties = {REPLICA_LEGACY_CONS: 'off'}
+ self.master.replica.setProperties(suffix=NEW_SUFFIX_1,
properties=properties)
+ except Exception as e:
+ self.master.log.info("Exception (expected): %s" %
type(e).__name__)
+ assert isinstance(e, NotImplementedError)
+
- conn.delete_s(','.join(['cn="o=testReplicaCreation"',
DN_MAPPING_TREE]))
- conn.backend.delete('testReplicaCreationdb')
-
-
-def changelog():
- changelog_e = conn.replica.changelog(dbname='foo')
- assert changelog_e.data['nsslapd-changelogdir'].endswith('foo')
-
-
-def changelog_default_test():
- e = conn.replica.changelog()
- conn.added_entries.append(e.dn)
- assert e.dn, "Bad changelog entry: %r " % e
- assert e.getValue('nsslapd-changelogdir').endswith("changelogdb"),
"Mismatching entry %r " % e.data.get('nsslapd-changelogdir')
- conn.delete_s("cn=changelog5,cn=config")
-
-
-def changelog_customdb_test():
- e = conn.replica.changelog(dbname="mockChangelogDb")
- conn.added_entries.append(e.dn)
- assert e.dn, "Bad changelog entry: %r " % e
- assert
e.getValue('nsslapd-changelogdir').endswith("mockChangelogDb"),
"Mismatching entry %r " % e.data.get('nsslapd-changelogdir')
- conn.delete_s("cn=changelog5,cn=config")
-
-
-def changelog_full_path_test():
- e = conn.replica.changelog(dbname="/tmp/mockChangelogDb")
- conn.added_entries.append(e.dn)
-
- assert e.dn, "Bad changelog entry: %r " % e
- expect(e, 'nsslapd-changelogdir', "/tmp/mockChangelogDb")
- conn.delete_s("cn=changelog5,cn=config")
-
-
-def check_init_test():
- raise NotImplementedError()
-
-
-def disable_logging_test():
- raise NotImplementedError()
-
-
-def enable_logging_test():
- raise NotImplementedError()
-
-
-def status_test():
- status = conn.agreement.status(conn.agreement_dn)
- log.info(status)
- assert status
-
-
-def list_test():
- # was get_entries_test():
- replicas = conn.replica.list()
- assert any(['testreplica' in x.dn.lower() for x in replicas])
-
-
-def ruv_test():
- ruv = conn.replica.ruv(suffix='o=testReplica')
- assert ruv, "Missing RUV"
- assert len(ruv.rid), "Missing RID"
- assert int(MOCK_REPLICA_ID) in ruv.rid.keys()
-
-
-(a)raises(ldap.NO_SUCH_OBJECT)
-def ruv_missing_test():
- ruv = conn.replica.ruv(suffix='o=MISSING')
- assert ruv, "Missing RUV"
- assert len(ruv.rid), "Missing RID"
- assert int(MOCK_REPLICA_ID) in ruv.rid.keys()
-
-
-def start_test():
- raise NotImplementedError()
-
-
-def stop_test():
- raise NotImplementedError()
-
-
-def restart_test():
- raise NotImplementedError()
-
-
-def start_async_test():
- raise NotImplementedError()
-
-
-def wait_for_init_test():
- raise NotImplementedError()
-
-
-def setup_agreement_default_test():
- user = {
- 'binddn': DN_RMANAGER,
- 'bindpw': "password"
- }
- params = {'consumer': MockDirSrv(), 'suffix':
"o=testReplica"}
- params.update(user)
-
- agreement_dn = conn.replica.agreement_add(**params)
- conn.added_entries.append(agreement_dn)
-
-(a)raises(ldap.ALREADY_EXISTS)
-def setup_agreement_duplicate_test():
- user = {
- 'binddn': DN_RMANAGER,
- 'bindpw': "password"
- }
- params = {
- 'consumer': MockDirSrv(),
- 'suffix': "o=testReplica",
- 'cn_format': 'testAgreement',
- 'description_format': 'testAgreement'
- }
- params.update(user)
- conn.replica.agreement_add(**params)
-
-
-def setup_agreement_test():
- user = {
- 'binddn': DN_RMANAGER,
- 'bindpw': "password"
- }
- params = {'consumer': MockDirSrv(), 'suffix':
"o=testReplica"}
- params.update(user)
-
- conn.replica.agreement_add(**params)
- # timeout=120, auto_init=False, bindmethod='simple', starttls=False,
args=None):
- raise NotImplementedError()
-
-def setup_agreement_fractional_test():
- # TODO: fractiona replicates only a subset of attributes
- #
- user = {
- 'binddn': DN_RMANAGER,
- 'bindpw': "password"
- }
- params = {'consumer': MockDirSrv(), 'suffix':
"o=testReplica"}
- params.update(user)
-
- #conn.replica.agreement_add(**params)
- #cn_format=r'meTo_%s:%s', description_format=r'me to %s:%s',
timeout=120, auto_init=False, bindmethod='simple', starttls=False, args=None):
- raise NotImplementedError()
-
-
-def find_agreements_test():
- agreements = conn.replica.agreements(dn=False)
- assert any(['testagreement' in x.dn.lower(
- ) for x in agreements]), "Missing agreement"
-
-
-def find_agreements_dn_test():
- agreements_dn = conn.replica.agreements()
- assert any(['testagreement' in x.lower(
- ) for x in agreements_dn]), "Missing agreement"
-
-
-def setup_replica_test():
- args = {
- 'suffix': "o=testReplicaCreation",
- 'binddn': DN_RMANAGER,
- 'bindpw': "password",
- 'rtype': lib389.MASTER_TYPE,
- 'rid': MOCK_REPLICA_ID
- }
- # create a replica entry
- replica_e = conn.replica.add(**args)
- assert 'dn' in replica_e, "Missing dn in replica"
- conn.added_entries.append(replica_e['dn'])
-
-
-def setup_replica_hub_test():
- args = {
- 'suffix': "o=testReplicaCreation",
- 'binddn': DN_RMANAGER,
- 'bindpw': "password",
- 'rtype': lib389.HUB_TYPE,
- 'rid': MOCK_REPLICA_ID
- }
- # create a replica entry
- replica_e = conn.replica.add(**args)
- assert 'dn' in replica_e, "Missing dn in replica"
- conn.added_entries.append(replica_e['dn'])
-
-
-def setup_replica_referrals_test():
- #tombstone_purgedelay=None, purgedelay=None, referrals=None, legacy=False
- raise NotImplementedError()
-
-
-def setup_all_replica_test():
- raise NotImplementedError()
-
-def replica_keep_in_sync_test():
- raise NotImplementedError()
+ def test_delete(self):
+ self.master.log.info("\n\n#########################\n###
DELETE\n#########################")
+ '''
+ Currently not implemented
+ '''
+ try:
+ self.master.replica.delete(suffix=NEW_SUFFIX_4)
+ except Exception as e:
+ self.master.log.info("Exception (expected): %s" %
type(e).__name__)
+ assert isinstance(e, NotImplementedError)
+
+
+if __name__ == "__main__":
+ test = Test_replica()
+ test.setUp()
+
+ #time.sleep(30)
+ test.test_create()
+ test.test_list()
+ test.test_create_repl_manager()
+ test.test_enableReplication()
+ test.test_disableReplication()
+ test.test_setProperties()
+ test.test_getProperties()
+ test.test_delete()
+
+ test.tearDown()
+
+
+
+#
+#
+# conn = DirSrv()
+# def changelog():
+# changelog_e = conn.replica.changelog(dbname='foo')
+# assert changelog_e.data['nsslapd-changelogdir'].endswith('foo')
+#
+#
+# def changelog_default_test():
+# e = conn.replica.changelog()
+# conn.added_entries.append(e.dn)
+# assert e.dn, "Bad changelog entry: %r " % e
+# assert
e.getValue('nsslapd-changelogdir').endswith("changelogdb"),
"Mismatching entry %r " % e.data.get('nsslapd-changelogdir')
+# conn.delete_s("cn=changelog5,cn=config")
+#
+#
+# def changelog_customdb_test():
+# e = conn.replica.changelog(dbname="mockChangelogDb")
+# conn.added_entries.append(e.dn)
+# assert e.dn, "Bad changelog entry: %r " % e
+# assert
e.getValue('nsslapd-changelogdir').endswith("mockChangelogDb"),
"Mismatching entry %r " % e.data.get('nsslapd-changelogdir')
+# conn.delete_s("cn=changelog5,cn=config")
+#
+#
+# def changelog_full_path_test():
+# e = conn.replica.changelog(dbname="/tmp/mockChangelogDb")
+# conn.added_entries.append(e.dn)
+#
+# assert e.dn, "Bad changelog entry: %r " % e
+# expect(e, 'nsslapd-changelogdir', "/tmp/mockChangelogDb")
+# conn.delete_s("cn=changelog5,cn=config")
+#
+# def ruv_test():
+# ruv = conn.replica.ruv(suffix='o=testReplica')
+# assert ruv, "Missing RUV"
+# assert len(ruv.rid), "Missing RID"
+# assert int(MOCK_REPLICA_ID) in ruv.rid.keys()
+#
+#
+# @raises(ldap.NO_SUCH_OBJECT)
+# def ruv_missing_test():
+# ruv = conn.replica.ruv(suffix='o=MISSING')
+# assert ruv, "Missing RUV"
+# assert len(ruv.rid), "Missing RID"
+# assert int(MOCK_REPLICA_ID) in ruv.rid.keys()
+#
+#
+# def start_test():
+# raise NotImplementedError()
+#
+#
+# def stop_test():
+# raise NotImplementedError()
+#
+#
+# def restart_test():
+# raise NotImplementedError()
+#
+#
+# def start_async_test():
+# raise NotImplementedError()
+#
+#
+# def wait_for_init_test():
+# raise NotImplementedError()
+#
+#
+# def setup_agreement_default_test():
+# user = {
+# 'binddn': DN_RMANAGER,
+# 'bindpw': "password"
+# }
+# params = {'consumer': MockDirSrv(), 'suffix':
"o=testReplica"}
+# params.update(user)
+#
+# agreement_dn = conn.replica.agreement_add(**params)
+# conn.added_entries.append(agreement_dn)
+#
+# @raises(ldap.ALREADY_EXISTS)
+# def setup_agreement_duplicate_test():
+# user = {
+# 'binddn': DN_RMANAGER,
+# 'bindpw': "password"
+# }
+# params = {
+# 'consumer': MockDirSrv(),
+# 'suffix': "o=testReplica",
+# 'cn_format': 'testAgreement',
+# 'description_format': 'testAgreement'
+# }
+# params.update(user)
+# conn.replica.agreement_add(**params)
+#
+#
+# def setup_agreement_test():
+# user = {
+# 'binddn': DN_RMANAGER,
+# 'bindpw': "password"
+# }
+# params = {'consumer': MockDirSrv(), 'suffix':
"o=testReplica"}
+# params.update(user)
+#
+# conn.replica.agreement_add(**params)
+# # timeout=120, auto_init=False, bindmethod='simple', starttls=False,
args=None):
+# raise NotImplementedError()
+#
+# def setup_agreement_fractional_test():
+# # TODO: fractiona replicates only a subset of attributes
+# #
+# user = {
+# 'binddn': DN_RMANAGER,
+# 'bindpw': "password"
+# }
+# params = {'consumer': MockDirSrv(), 'suffix':
"o=testReplica"}
+# params.update(user)
+#
+# #conn.replica.agreement_add(**params)
+# #cn_format=r'meTo_%s:%s', description_format=r'me to %s:%s',
timeout=120, auto_init=False, bindmethod='simple', starttls=False, args=None):
+# raise NotImplementedError()
+#
+#
+# def find_agreements_test():
+# agreements = conn.replica.agreements(dn=False)
+# assert any(['testagreement' in x.dn.lower(
+# ) for x in agreements]), "Missing agreement"
+#
+#
+# def find_agreements_dn_test():
+# agreements_dn = conn.replica.agreements()
+# assert any(['testagreement' in x.lower(
+# ) for x in agreements_dn]), "Missing agreement"
+#
+#
+# def setup_replica_test():
+# args = {
+# 'suffix': "o=testReplicaCreation",
+# 'binddn': DN_RMANAGER,
+# 'bindpw': "password",
+# 'rtype': lib389.MASTER_TYPE,
+# 'rid': MOCK_REPLICA_ID
+# }
+# # create a replica entry
+# replica_e = conn.replica.add(**args)
+# assert 'dn' in replica_e, "Missing dn in replica"
+# conn.added_entries.append(replica_e['dn'])
+#
+#
+# def setup_replica_hub_test():
+# args = {
+# 'suffix': "o=testReplicaCreation",
+# 'binddn': DN_RMANAGER,
+# 'bindpw': "password",
+# 'rtype': lib389.HUB_TYPE,
+# 'rid': MOCK_REPLICA_ID
+# }
+# # create a replica entry
+# replica_e = conn.replica.add(**args)
+# assert 'dn' in replica_e, "Missing dn in replica"
+# conn.added_entries.append(replica_e['dn'])
+#
+#
+# def setup_replica_referrals_test():
+# #tombstone_purgedelay=None, purgedelay=None, referrals=None, legacy=False
+# raise NotImplementedError()
+#
+#
+# def setup_all_replica_test():
+# raise NotImplementedError()
+#
+# def replica_keep_in_sync_test():
+# raise NotImplementedError()
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.