From 27536f985505052197f1ff7ad74d492d8bf1ca39 Mon Sep 17 00:00:00 2001
From: "Thierry bordaz (tbordaz)" <tbordaz@redhat.com>
Date: Mon, 6 Jan 2014 17:26:05 +0100
Subject: [PATCH] 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:
	Move

https://fedorahosted.org/389/ticket/47600

Reviewed by: ?

Platforms tested: F17

Flag Day: no

Doc impact: no
---
 lib389/__init__.py   |  81 +-----
 lib389/_constants.py |  29 +-
 lib389/_entry.py     |  34 +++
 lib389/agreement.py  | 626 ++++++++++++++++++++++++++++++++++++++++
 lib389/brooker.py    | 798 ---------------------------------------------------
 lib389/changelog.py  |  96 +++++++
 lib389/properties.py | 156 ++++++++--
 lib389/replica.py    | 550 +++++++++++++++++++++++++++++++++++
 lib389/suffix.py     |   8 +
 9 files changed, 1479 insertions(+), 899 deletions(-)
 create mode 100644 lib389/agreement.py
 create mode 100644 lib389/changelog.py
 create mode 100644 lib389/replica.py

diff --git a/lib389/__init__.py b/lib389/__init__.py
index 60ac2f2..6e2b357 100644
--- a/lib389/__init__.py
+++ b/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/lib389/_constants.py b/lib389/_constants.py
index 35fbcf3..3557ec3 100644
--- a/lib389/_constants.py
+++ b/lib389/_constants.py
@@ -1,7 +1,7 @@
 # replicatype @see https://access.redhat.com/knowledge/docs/en-US/Red_Hat_Directory_Server/8.1/html/Administration_Guide/Managing_Replication-Configuring-Replication-cmd.html
 # 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,27 @@ 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'
+
+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]
 }
 
 
@@ -51,6 +62,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 +89,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/lib389/_entry.py b/lib389/_entry.py
index 6e3dcd0..f993a36 100644
--- a/lib389/_entry.py
+++ b/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/lib389/agreement.py b/lib389/agreement.py
new file mode 100644
index 0000000..37f720c
--- /dev/null
+++ b/lib389/agreement.py
@@ -0,0 +1,626 @@
+'''
+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)
+        
+        # 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]+
+                    
+            @return - None
+            
+            @raise ValueError - if interval is not valid
+                   ldap.NO_SUCH_OBJECT - if agmtdn does not exist
+        """
+        
+        # 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, agmtdn, 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
+
+            @return - returns a dictionary of the properties
+
+            @raise - None
+            
+            supported properties are
+                property name                       Replication Agreement attribute name
+                'schedule'                       -> 'nsds5replicaupdateschedule'
+                '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 invalid property name
+                   ldap.NO_SUCH_OBJECT - if agmtdn does not exist
+                   InvalidArgumentError - missing mandatory argument
+            
+            
+        ''' 
+        
+        if not agmtdn:
+            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 = []
+        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.getEntry(agmtdn, 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 props.len() > 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.keys():
+            
+            # 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.getEntry(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
+            filt = "(&(%s=%s)(%s=%d))" % (RA_CONSUMER_HOST, consumer_host, RA_CONSUMER_PORT, consumer_port)
+            return self.getEntry(replica_entry.dn, ldap.SCOPE_ONELEVEL, filt)
+        
+        
+    def create(self, suffix=None, host=None, port=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')
+        
+        if args:
+            binddn      = args[RA_BINDDN]       or defaultProperties[REPLICATION_BIND_DN]
+            bindpw      = args[RA_BINDPW]       or defaultProperties[REPLICATION_BIND_PW]
+            bindmethod  = args[RA_METHOD]       or defaultProperties[REPLICATION_BIND_METHOD]
+            format      = args[RA_NAME]         or r'meTo_$host:$port'
+            description = args[RA_DESCRIPTION]  or format
+            transport   = args[RA_TRANSPORT_PROT] or defaultProperties[REPLICATION_TRANSPORT]
+        else:
+            binddn      = defaultProperties[REPLICATION_BIND_DN]
+            bindpw      = defaultProperties[REPLICATION_BIND_PW]
+            bindmethod  = defaultProperties[REPLICATION_BIND_METHOD]
+            format      = r'meTo_$host:$port'
+            description = args[RA_DESCRIPTION]  or format
+            transport   = args[RA_TRANSPORT_PROT] or defaultProperties[REPLICATION_TRANSPORT]
+        
+        # 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': 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]:  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 args dict
+        argscopy = {}
+        if args:
+            import copy
+            argscopy = copy.deepcopy(args)
+
+        # further arguments
+        if 'winsync' in argscopy:  # state it clearly!
+            self.conn.setupWinSyncAgmt(argscopy, 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 argscopy:
+                raise NotImplemented
+                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': 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".
+          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, agmtdn):
+        """Return a list of changes sent by this agreement."""
+        retval = 0
+        try:
+            ent = self.conn.getEntry(
+                agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", [ RA_PROPNAME_TO_ATTRNAME[RA_CHANGES] ])
+        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
+        
\ No newline at end of file
diff --git a/lib389/brooker.py b/lib389/brooker.py
index 3d43009..1446978 100644
--- a/lib389/brooker.py
+++ b/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/lib389/changelog.py b/lib389/changelog.py
new file mode 100644
index 0000000..53c5592
--- /dev/null
+++ b/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/lib389/properties.py b/lib389/properties.py
index 07b4d0d..4b5184a 100644
--- a/lib389/properties.py
+++ b/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/lib389/replica.py b/lib389/replica.py
new file mode 100644
index 0000000..40cb24c
--- /dev/null
+++ b/lib389/replica.py
@@ -0,0 +1,550 @@
+'''
+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
+            
+            @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.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 NotImplemented
+
+    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
+        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 NotImplemented()
+        
+    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):
+        if not suffix:
+            self.log.fatal("enableReplication: suffix not specified")
+            raise ValueError("suffix missing")
+        
+        raise NotImplemented
+    
+    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/lib389/suffix.py b/lib389/suffix.py
index bd6d346..2e3f0aa 100644
--- a/lib389/suffix.py
+++ b/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")
 
-- 
1.7.11.7

