Exemple #1
0
    def list(self,
             suffix=None,
             consumer_host=None,
             consumer_port=None,
             agmtdn=None):
        '''
            Returns the search result of the replica agreement(s) under the replica (replicaRoot is 'suffix').

            Either 'suffix' or 'agmtdn' need to be specfied.
            'consumer_host' and 'consumer_port' are either not specified or specified both.

            If 'agmtdn' is specified, it returns the search result entry of that replication agreement.
            else if consumer host/port are specified it returns the replica agreements toward
            that consumer host:port.
            Finally if neither 'agmtdn' nor 'consumser host/port' are specifies it returns
            all the replica agreements under the replica (replicaRoot is 'suffix').

            @param - suffix is the suffix targeted by the total update
            @param - consumer_host hostname of the consumer
            @param - consumer_port port of the consumer
            @param - agmtdn DN of the replica agreement

            @return - search result of the replica agreements

            @raise - InvalidArgument: if missing mandatory argument (agmtdn or suffix, then host and port)
                   - ValueError - if some properties are not valid
                   - NoSuchEntryError - If no replica defined for the suffix
        '''
        if not suffix and not agmtdn:
            raise InvalidArgumentError("suffix or agmtdn are required")

        if (consumer_host and not consumer_port) or (not consumer_host
                                                     and consumer_port):
            raise InvalidArgumentError(
                "consumer host/port are required together")

        if agmtdn:
            # easy case, just return the RA
            filt = "objectclass=*"
            return self.conn.search_s(agmtdn, ldap.SCOPE_BASE, filt)
        else:
            # Retrieve the replica
            replica_entries = self.conn.replica.list(suffix)
            if not replica_entries:
                raise NoSuchEntryError("Error: no replica set up for suffix " +
                                       suffix)
            replica_entry = replica_entries[0]

            # Now returns the replica agreement for that suffix that replicates to
            # consumer host/port
            if consumer_host and consumer_port:
                filt = "(&(|(objectclass=%s)(objectclass=%s))(%s=%s)(%s=%d))" % (
                    RA_OBJECTCLASS_VALUE, RA_WINDOWS_OBJECTCLASS_VALUE,
                    RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_HOST], consumer_host,
                    RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_PORT], consumer_port)
            else:
                filt = "(|(objectclass=%s)(objectclass=%s))" % (
                    RA_OBJECTCLASS_VALUE, RA_WINDOWS_OBJECTCLASS_VALUE)
            return self.conn.search_s(replica_entry.dn, ldap.SCOPE_ONELEVEL,
                                      filt)
Exemple #2
0
    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)
Exemple #3
0
    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)
Exemple #4
0
    def schedule(self, agmtdn=None, interval=ALWAYS):
        """Schedule the replication agreement

        :param agmtdn: DN of the replica agreement
        :type agmtdn: str
        :param interval: - 'HHMM-HHMM D+' With D=[0123456]+
                          - Agreement.ALWAYS
                          - Agreement.NEVER
        :type interval: str

        :returns: None
        :raises: - ValueError - if interval is not valid;
                  - ldap.NO_SUCH_OBJECT - if agmtdn does not exist
        """
        if not agmtdn:
            raise InvalidArgumentError("agreement DN is missing")

        # check the validity of the interval
        if interval != Agreement.ALWAYS and interval != Agreement.NEVER:
            self._check_interval(interval)

        # Check if the replica agreement exists
        try:
            self.conn.getEntry(agmtdn, ldap.SCOPE_BASE)
        except ldap.NO_SUCH_OBJECT:
            raise

        # update it
        self.log.info("Schedule replication agreement %s" % agmtdn)
        mod = [(ldap.MOD_REPLACE, 'nsds5replicaupdateschedule',
                [ensure_bytes(interval)])]
        self.conn.modify_s(agmtdn, mod)
Exemple #5
0
    def disable(self, name=None, plugin_dn=None):
        '''
            Disable a plugin
            
            If 'plugin_dn' and 'name' are provided, plugin_dn is used and 'name is not considered
            
            @param name - name of the plugin
            @param plugin_dn - DN of the plugin
            
            @return None
            
            @raise ValueError - if 'name' or 'plugin_dn' lead to unknown plugin
                    InvalidArgumentError - if 'name' and 'plugin_dn' are missing
        '''

        dn = plugin_dn or "cn=%s,%s" % (name, DN_PLUGIN)
        filt = "(objectclass=%s)" % PLUGINS_OBJECTCLASS_VALUE

        if not dn:
            raise InvalidArgumentError(
                "plugin 'name' and 'plugin_dn' are missing")

        ents = self.conn.search_s(dn, ldap.SCOPE_BASE, filt)
        if len(ents) != 1:
            raise ValueError("%s is unknown")

        self.conn.modify_s(
            dn, [(ldap.MOD_REPLACE, PLUGIN_PROPNAME_TO_ATTRNAME[PLUGIN_ENABLE],
                  PLUGINS_ENABLE_OFF_VALUE)])
Exemple #6
0
    def getParent(self, suffix=None):
        '''
            Returns the DN of a suffix that is the parent of the provided 'suffix'. 
            If 'suffix' has no parent, it returns None
            
            @param suffix - suffix DN of the backend
            
            @return parent suffix DN
            
            @return ValueError - if suffix is not provided
                    InvalidArgumentError - if suffix is not implemented on the server

        '''
        if not suffix:
            raise ValueError("suffix is mandatory")

        ents = self.conn.mappingtree.list(suffix=suffix)
        if len(ents) == 0:
            raise InvalidArgumentError(
                "suffix %s is not implemented on that server" % suffix)

        mapping_tree = ents[0]
        if mapping_tree.hasValue(MT_PROPNAME_TO_ATTRNAME[MT_PARENT_SUFFIX]):
            return mapping_tree.getValue(
                MT_PROPNAME_TO_ATTRNAME[MT_PARENT_SUFFIX])
        else:
            return None

        raise NotImplemented
Exemple #7
0
    def deleteAgreements(self, suffix=None):
        '''
        Delete all the agreements for the suffix
        '''
        # check the validity of the suffix
        if not suffix:
            self.log.fatal("disableReplication: suffix is missing")
            raise InvalidArgumentError("suffix missing")
        else:
            nsuffix = normalizeDN(suffix)

        # Build the replica config DN
        mtents = self.conn.mappingtree.list(suffix=nsuffix)
        mtent = mtents[0]
        dn_replica = ','.join((RDN_REPLICA, mtent.dn))

        # Delete the agreements
        try:
            agmts = self.conn.agreement.list(suffix=suffix)
            for agmt in agmts:
                try:
                    self.conn.delete_s(agmt.dn)
                except ldap.LDAPError, e:
                    self.log.fatal(
                        'Failed to delete replica agreement (%s), error: %s' %
                        (admt.dn, e.message('desc')))
                    raise ldap.LDAPError
        except ldap.LDAPError, e:
            self.log.fatal(
                'Failed to search for replication agreements under (%s), error: %s'
                % (dn_replica, e.message('desc')))
            raise ldap.LDAPError
Exemple #8
0
    def disableReplication(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 InvalidArgumentError - if suffix is missing
                   ldap.LDAPError - for all other update failures

        '''

        # check the validity of the suffix
        if not suffix:
            self.log.fatal("disableReplication: suffix is missing")
            raise InvalidArgumentError("suffix missing")
        else:
            nsuffix = normalizeDN(suffix)

        # Build the replica config DN
        mtents = self.conn.mappingtree.list(suffix=nsuffix)
        mtent = mtents[0]
        dn_replica = ','.join((RDN_REPLICA, mtent.dn))

        # Delete the agreements
        try:
            self.deleteAgreements(nsuffix)
        except ldap.LDAPError, e:
            self.log.fatal('Failed to delete replica agreements!')
            raise ldap.LDAPError
Exemple #9
0
 def toSuffix(self, entry=None, name=None):
     '''
         Return, for a given backend entry, the suffix values.
         Suffix values are identical from a LDAP point of views. Suffix values may
         be surrounded by ", or containing '\' escape characters.
         
         @param entry - LDAP entry of the backend
         @param name  - backend DN
         
         @result list of values of suffix attribute (aka 'cn')
         
         @raise ldap.NO_SUCH_OBJECT - in name is invalid DN
                ValueError - entry does not contains the suffix attribute
                InvalidArgumentError - if both entry/name are missing
     '''
     attr_suffix = BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_SUFFIX]
     if entry:
         if not entry.hasValue(attr_suffix):
             raise ValueError("Entry has no %s attribute %r" % (attr_suffix, entry))
         return entry.getValues(attr_suffix)
     elif name:
         filt = "(objectclass=%s)" % BACKEND_OBJECTCLASS_VALUE
         
         try:
             attrs = [attr_suffix]
             ent = self.conn.getEntry(name, ldap.SCOPE_BASE, filt, attrs)
             self.log.debug("toSuffix: %s found by its DN" % ent.dn)
         except NoSuchEntryError:
             raise ldap.NO_SUCH_OBJECT("Backend DN not found: %s" % name)
         
         if not ent.hasValue(attr_suffix):
             raise ValueError("Entry has no %s attribute %r" % (attr_suffix, ent))
         return ent.getValues(attr_suffix)
     else:
         raise InvalidArgumentError("entry or name are mandatory")
Exemple #10
0
    def getProperties(self, agmnt_dn=None, properties=None):
        """Get a dictionary of the requested properties.
        If properties parameter is missing, it returns all the properties.

        :param agmtdn: DN of the replica agreement
        :type agmtdn: str
        :param properties: List of properties name
        :type properties: list

        :returns: Returns a dictionary of the properties
        :raises: - ValueError - if invalid property name
                  - ldap.NO_SUCH_OBJECT - if agmtdn does not exist
                  - InvalidArgumentError - missing mandatory argument

        :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
        """

        if not agmnt_dn:
            raise InvalidArgumentError("agmtdn is a mandatory argument")

        #
        # prepare the attribute list to retrieve from the RA
        # if properties is None, all RA attributes are retrieved
        #
        attrs = []
        if properties:
            for prop_name in properties:
                prop_attr = RA_PROPNAME_TO_ATTRNAME[prop_name]
                if not prop_attr:
                    raise ValueError("Improper property name: %s ", prop_name)
                attrs.append(prop_attr)

        filt = "(objectclass=*)"
        result = {}
        try:
            entry = self.conn.getEntry(agmnt_dn, ldap.SCOPE_BASE, filt, attrs)

            # Build the result from the returned attributes
            for attr in entry.getAttrs():
                # given an attribute name retrieve the property name
                props = [
                    k for k, v in six.iteritems(RA_PROPNAME_TO_ATTRNAME)
                    if v.lower() == attr.lower()
                ]

                # If this attribute is present in the RA properties, adds it to
                # result
                if len(props) > 0:
                    result[props[0]] = entry.getValues(attr)

        except ldap.NO_SUCH_OBJECT:
            raise

        return result
Exemple #11
0
    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
Exemple #12
0
    def setProperties(self, changelogdn=None, properties=None):
        """Set the properties of the changelog entry.

        :param changelogdn: DN of the changelog
        :type changelogdn: str
        :param properties: Dictionary of properties
        :type properties: dict

        :returns: None
        :raises: - ValueError - if invalid properties
                 - ValueError - if changelog entry is not found
                 - InvalidArgumentError - changelog DN is missing

        :supported properties are:
                CHANGELOG_NAME, CHANGELOG_DIR, CHANGELOG_MAXAGE,
                CHANGELOG_MAXENTRIES, CHANGELOG_TRIM_INTERVAL,
                CHANGELOG_COMPACT_INTV, CHANGELOG_CONCURRENT_WRITES,
                CHANGELOG_ENCRYPT_ALG, CHANGELOG_SYM_KEY
        """

        if not changelogdn:
            raise InvalidArgumentError("changelog DN is missing")

        ents = self.conn.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, CHANGELOG_PROPNAME_TO_ATTRNAME[val], properties[prop]))

        # that is fine now to apply the MOD
        self.conn.modify_s(ents[0].dn, mods)
Exemple #13
0
    def delete(self,
               suffix=None,
               consumer_host=None,
               consumer_port=None,
               agmtdn=None):
        """Delete a replication agreement

        :param suffix: The suffix that the agreement is configured for
        :type suffix: str
        :param consumer_host: Host of the server that the agreement points to
        :type consumer_host: str
        :param consumer_port: Port of the server that the agreement points to
        :type consumer_port: int
        :param agmtdn: DN of the replica agreement
        :type agmtdn: str

        :returns: None
        :raises: - ldap.LDAPError - for ldap operation failures
                  - TypeError - if too many agreements were found
                  - NoSuchEntryError - if no agreements were found
        """

        if not (suffix and consumer_host and consumer_port) and not agmtdn:
            raise InvalidArgumentError(
                "Suffix with consumer_host and consumer_port" +
                " or agmtdn are required")

        agmts = self.list(suffix, consumer_host, consumer_port, agmtdn)

        if agmts:
            if len(agmts) > 1:
                raise TypeError('Too many agreements were found')
            else:
                # Delete the agreement
                try:
                    agmt_dn = agmts[0].dn
                    self.conn.delete_s(agmt_dn)
                    self.log.info('Agreement (%s) was successfully removed',
                                  agmt_dn)
                except ldap.LDAPError as e:
                    self.log.error(
                        'Failed to delete agreement (%s), '
                        'error: %s', agmt_dn, e)
                    raise
        else:
            raise NoSuchEntryError('No agreements were found')
Exemple #14
0
    def create(self, suffix=None, host=None, port=None, properties=None):
        """Create (and return) a replication agreement from self to consumer.
            - self is the supplier,

            @param consumer: one of the following (consumer can be a master)
                    * a DirSrv object if chaining
                    * an object with attributes: host, port, sslport, __str__
            @param suffix    - eg. 'dc=babel,dc=it'
            @param properties      - further properties dict.
            Support properties
                RA_NAME
                RA_SUFFIX
                RA_BINDDN
                RA_BINDPW
                RA_METHOD
                RA_DESCRIPTION
                RA_SCHEDULE
                RA_TRANSPORT_PROT
                RA_FRAC_EXCLUDE
                RA_FRAC_EXCLUDE_TOTAL_UPDATE
                RA_FRAC_STRIP
                RA_CONSUMER_PORT
                RA_CONSUMER_HOST
                RA_CONSUMER_TOTAL_INIT
                RA_TIMEOUT
                RA_CHANGES


            @return dn_agreement - DN of the created agreement

            @raise InvalidArgumentError - If the suffix is missing
            @raise NosuchEntryError     - if a replica doesn't exist for that suffix
            @raise UNWILLING_TO_PERFORM if the database was previously
                    in read-only state. To create new agreements you
                    need to *restart* the directory server

        """
        import string

        # Check we have a suffix [ mandatory ]
        if not suffix:
            self.log.warning("create: suffix is missing")
            raise InvalidArgumentError('suffix is mandatory')

        if properties:
            binddn = properties.get(
                RA_BINDDN) or defaultProperties[REPLICATION_BIND_DN]
            bindpw = properties.get(
                RA_BINDPW) or defaultProperties[REPLICATION_BIND_PW]
            bindmethod = properties.get(
                RA_METHOD) or defaultProperties[REPLICATION_BIND_METHOD]
            format = properties.get(RA_NAME) or r'meTo_$host:$port'
            description = properties.get(RA_DESCRIPTION) or format
            transport = properties.get(
                RA_TRANSPORT_PROT) or defaultProperties[REPLICATION_TRANSPORT]
            timeout = properties.get(
                RA_TIMEOUT) or defaultProperties[REPLICATION_TIMEOUT]
        else:
            binddn = defaultProperties[REPLICATION_BIND_DN]
            bindpw = defaultProperties[REPLICATION_BIND_PW]
            bindmethod = defaultProperties[REPLICATION_BIND_METHOD]
            format = r'meTo_$host:$port'
            description = format
            transport = defaultProperties[REPLICATION_TRANSPORT]
            timeout = defaultProperties[REPLICATION_TIMEOUT]

        # Compute the normalized suffix to be set in RA entry
        nsuffix = normalizeDN(suffix)

        # adding agreement under the replica entry
        replica_entries = self.conn.replica.list(suffix)
        if not replica_entries:
            raise NoSuchEntryError("Error: no replica set up for suffix " +
                                   suffix)
        replica = replica_entries[0]

        # define agreement entry
        cn = string.Template(format).substitute({'host': host, 'port': port})
        dn_agreement = ','.join(["cn=%s" % cn, replica.dn])

        # This is probably unnecessary because
        # we can just raise ALREADY_EXISTS
        try:

            entry = self.conn.getEntry(dn_agreement, ldap.SCOPE_BASE)
            self.log.warn("Agreement already exists: %r" % dn_agreement)
            return dn_agreement
        except ldap.NO_SUCH_OBJECT:
            entry = None

        # In a separate function in this scope?
        entry = Entry(dn_agreement)
        entry.update({
            'objectclass': ["top", RA_OBJECTCLASS_VALUE],
            RA_PROPNAME_TO_ATTRNAME[RA_NAME]:
            cn,
            RA_PROPNAME_TO_ATTRNAME[RA_SUFFIX]:
            nsuffix,
            RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_HOST]:
            host,
            RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_PORT]:
            str(port),
            RA_PROPNAME_TO_ATTRNAME[RA_TRANSPORT_PROT]:
            transport,
            RA_PROPNAME_TO_ATTRNAME[RA_TIMEOUT]:
            str(timeout),
            RA_PROPNAME_TO_ATTRNAME[RA_BINDDN]:
            binddn,
            RA_PROPNAME_TO_ATTRNAME[RA_BINDPW]:
            bindpw,
            RA_PROPNAME_TO_ATTRNAME[RA_METHOD]:
            bindmethod,
            RA_PROPNAME_TO_ATTRNAME[RA_DESCRIPTION]:
            string.Template(description).substitute({
                'host': host,
                'port': port
            })
        })

        # we make a copy here because we cannot change
        # the passed in properties dict
        propertiescopy = {}
        if properties:
            import copy
            propertiescopy = copy.deepcopy(properties)

        # further arguments
        if 'winsync' in propertiescopy:  # state it clearly!
            self.conn.setupWinSyncAgmt(propertiescopy, entry)

        try:
            self.log.debug("Adding replica agreement: [%r]" % entry)
            self.conn.add_s(entry)
        except:
            #  FIXME check please!
            raise

        entry = self.conn.waitForEntry(dn_agreement)
        if entry:
            # More verbose but shows what's going on
            if 'chain' in propertiescopy:
                raise NotImplementedError
                chain_args = {
                    'suffix': suffix,
                    'binddn': binddn,
                    'bindpw': bindpw
                }
                # Work on `self` aka producer
                if replica.nsds5replicatype == MASTER_TYPE:
                    self.conn.setupChainingFarm(**chain_args)
                # Work on `consumer`
                # TODO - is it really required?
                if replica.nsds5replicatype == LEAF_TYPE:
                    chain_args.update({
                        'isIntermediate': 0,
                        'urls': self.conn.toLDAPURL(),
                        'args': propertiescopy['chainargs']
                    })
                    consumer.setupConsumerChainOnUpdate(**chain_args)
                elif replica.nsds5replicatype == HUB_TYPE:
                    chain_args.update({
                        'isIntermediate': 1,
                        'urls': self.conn.toLDAPURL(),
                        'args': propertiescopy['chainargs']
                    })
                    consumer.setupConsumerChainOnUpdate(**chain_args)

        return dn_agreement
Exemple #15
0
    def create(self,
               suffix=None,
               host=None,
               port=None,
               properties=None,
               winsync=False):
        """Create (and return) a replication agreement from self to consumer.
        Self is the supplier.

        :param suffix: Replication Root
        :type suffix: str
        :param host: Consumer host
        :type host: str
        :param port: Consumer port
        :type port: int
        :param winsync: Identifies the agree as a WinSync agreement
        :type winsync: bool
        :param properties: Agreement properties
        :type properties: dict

        :returns: DN of the created agreement
        :raises: - InvalidArgumentError - If the suffix is missing
                 - NoSuchEntryError - if a replica doesn't exist for that suffix
                 - ldap.LDAPError - ldap error
        """

        # Check we have a suffix [ mandatory ]
        if not suffix:
            self.log.warning("create: suffix is missing")
            raise InvalidArgumentError('suffix is mandatory')

        if not properties:
            properties = {}

        # Compute the normalized suffix to be set in RA entry
        properties[RA_SUFFIX] = 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: %s" %
                                   suffix)
        replica = replica_entries[0]

        # Define agreement entry
        if RA_NAME not in properties:
            properties[RA_NAME] = 'meTo_%s:%s' % (host, port)
        dn_agreement = ','.join(["cn=%s" % properties[RA_NAME], replica.dn])

        # Set the required properties(if not already set)
        if RA_BINDDN not in properties:
            properties[RA_BINDDN] = defaultProperties[REPLICATION_BIND_DN]
        if RA_BINDPW not in properties:
            properties[RA_BINDPW] = defaultProperties[REPLICATION_BIND_PW]
        if RA_METHOD not in properties:
            properties[RA_METHOD] = defaultProperties[REPLICATION_BIND_METHOD]
        if RA_TRANSPORT_PROT not in properties:
            properties[RA_TRANSPORT_PROT] = \
                defaultProperties[REPLICATION_TRANSPORT]
        if RA_TIMEOUT not in properties:
            properties[RA_TIMEOUT] = defaultProperties[REPLICATION_TIMEOUT]
        if RA_DESCRIPTION not in properties:
            properties[RA_DESCRIPTION] = properties[RA_NAME]
        if RA_CONSUMER_HOST not in properties:
            properties[RA_CONSUMER_HOST] = host
        if RA_CONSUMER_PORT not in properties:
            properties[RA_CONSUMER_PORT] = str(port)

        # 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

        # Iterate over the properties, adding them to the entry
        entry = Entry(dn_agreement)
        entry.update({'objectclass': ["top", RA_OBJECTCLASS_VALUE]})
        for prop in properties:
            entry.update({RA_PROPNAME_TO_ATTRNAME[prop]: properties[prop]})

        # we make a copy here because we cannot change
        # the passed in properties dict
        propertiescopy = {}
        if properties:
            import copy
            propertiescopy = copy.deepcopy(properties)

        # Check if this a Winsync Agreement
        if winsync:
            self.conn.setupWinSyncAgmt(propertiescopy, entry)

        try:
            self.log.debug("Adding replica agreement: [%r]" % entry)
            self.conn.add_s(entry)
        except ldap.LDAPError as e:
            self.log.fatal('Failed to add replication agreement: %s' % str(e))
            raise e

        entry = self.conn.waitForEntry(dn_agreement)
        if entry:
            # More verbose but shows what's going on
            if 'chain' in propertiescopy:
                raise NotImplementedError
                chain_args = {
                    'suffix': suffix,
                    'binddn': binddn,
                    'bindpw': bindpw
                }
                # Work on `self` aka producer
                if replica.nsds5replicatype == MASTER_TYPE:
                    self.conn.setupChainingFarm(**chain_args)
                # Work on `consumer`
                # TODO - is it really required?
                if replica.nsds5replicatype == LEAF_TYPE:
                    chain_args.update({
                        'isIntermediate': 0,
                        'urls': self.conn.toLDAPURL(),
                        'args': propertiescopy['chainargs']
                    })
                    consumer.setupConsumerChainOnUpdate(**chain_args)
                elif replica.nsds5replicatype == HUB_TYPE:
                    chain_args.update({
                        'isIntermediate': 1,
                        'urls': self.conn.toLDAPURL(),
                        'args': propertiescopy['chainargs']
                    })
                    consumer.setupConsumerChainOnUpdate(**chain_args)

        return dn_agreement
Exemple #16
0
    def setProperties(self,
                      suffix=None,
                      agmnt_dn=None,
                      agmnt_entry=None,
                      properties=None):
        """Set the properties of the agreement entry. If an 'agmnt_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)
        :type suffix: str
        :param agmnt_dn: DN of the agreement (online update)
        :type agmnt_dn: str
        :param agmnt_entry: Entry of a agreement (offline update)
        :type agmnt_entry: lib389.Entry
        :param properties: Dictionary of properties
        :type properties: dict

        :returns: None
        :raises: - ValueError - if invalid properties
                  - ValueError - if invalid agreement_entry
                  - ValueError - if agmnt_dn or suffix are not associated to a replica
                  - InvalidArgumentError - missing mandatory argument

        :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
        """

        # No properties provided
        if len(properties) == 0:
            return

        # check that the given properties are valid
        for prop in properties:
            # skip the prefix to add/del value
            if not inProperties(prop, RA_PROPNAME_TO_ATTRNAME):
                raise ValueError("unknown property: %s" % prop)
            else:
                self.log.debug("setProperties: %s:%s" %
                               (prop, properties[prop]))

        # At least we need to have suffix/agmnt_dn/agmnt_entry
        if not suffix and not agmnt_dn and not agmnt_entry:
            raise InvalidArgumentError("suffix and agmnt_dn and agmnt_entry " +
                                       "are missing")

        # TODO
        if suffix:
            raise NotImplemented

        # The caller provides a set of properties to set into a replica entry
        if agmnt_entry:
            if not isinstance(agmnt_entry, Entry):
                raise ValueError("invalid instance of the agmnt_entry")

            # that is fine, now set the values
            for prop in properties:
                val = rawProperty(prop)

                # for Entry update it is a replace
                agmnt_entry.update(
                    {RA_PROPNAME_TO_ATTRNAME[val]: properties[prop]})

            return

        # for each provided property build the mod
        mod = []
        for prop in properties:

            # retrieve/check the property name
            # and if the value needs to be added/deleted/replaced
            if prop.startswith('+'):
                mod_type = ldap.MOD_ADD
                prop_name = prop[1:]
            elif prop.startswith('-'):
                mod_type = ldap.MOD_DELETE
                prop_name = prop[1:]
            else:
                mod_type = ldap.MOD_REPLACE
                prop_name = prop

            attr = RA_PROPNAME_TO_ATTRNAME[prop_name]
            if not attr:
                raise ValueError("invalid property name %s" % prop_name)

            # Now do the value checking we can do
            if prop_name == RA_SCHEDULE:
                self._check_interval(properties[prop])

            mod.append((mod_type, attr, ensure_bytes(properties[prop])))

        # Now time to run the effective modify
        self.conn.modify_s(agmnt_dn, mod)
Exemple #17
0
    def create(self, suffix=None, properties=None):
        """
            Creates backend entry and returns its dn. 
            
            If the properties 'chain-bind-pwd' and 'chain-bind-dn' and 'chain-urls' are specified 
            the backend is a chained backend. 
            A chaining backend is created under 'cn=chaining database,cn=plugins,cn=config'. 
            
            A local backend is created under 'cn=ldbm database,cn=plugins,cn=config' 
            
            @param suffix - suffix stored in the backend
            @param properties - dictionary with properties values
            supported properties are
                BACKEND_NAME          = 'name'
                BACKEND_READONLY      = 'read-only'
                BACKEND_REQ_INDEX     = 'require-index'
                BACKEND_CACHE_ENTRIES = 'entry-cache-number'
                BACKEND_CACHE_SIZE    = 'entry-cache-size'
                BACKEND_DNCACHE_SIZE  = 'dn-cache-size'
                BACKEND_DIRECTORY     = 'directory'
                BACKEND_DB_DEADLOCK   = 'db-deadlock'
                BACKEND_CHAIN_BIND_DN = 'chain-bind-dn'
                BACKEND_CHAIN_BIND_PW = 'chain-bind-pw'
                BACKEND_CHAIN_URLS    = 'chain-urls'
                
            @return backend DN of the created backend
            
            @raise ValueError - If missing suffix 
                    InvalidArgumentError - If it already exists a backend for that suffix or a backend with the same DN

        """
        def _getBackendName(parent):
            '''
                Use to build a backend name that is not already used
            '''
            index = 1
            while True:
                bename = "local%ddb" % index
                base   = "%s=%s,%s" % (BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_NAME], bename, parent)
                scope  = ldap.SCOPE_BASE
                filt   = "(objectclass=%s)" % BACKEND_OBJECTCLASS_VALUE
                self.log.debug("_getBackendName: baser=%s : fileter=%s" % (base, filt))
                try:
                    ents = self.conn.getEntry(base, ldap.SCOPE_BASE, filt)
                except (NoSuchEntryError, ldap.NO_SUCH_OBJECT) as e:
                    self.log.info("backend name will be %s" % bename)
                    return bename
                index += 1
                
        # suffix is mandatory
        if not suffix:
            raise ValueError("suffix is mandatory")
        else:
            nsuffix = normalizeDN(suffix)
        
        # Check it does not already exist a backend for that suffix
        ents = self.conn.backend.list(suffix=suffix)
        if len(ents) != 0:
            raise InvalidArgumentError("It already exists backend(s) for %s: %s" % (suffix, ents[0].dn))
        
        # Check if we are creating a local/chained backend
        chained_suffix = properties and (BACKEND_CHAIN_BIND_DN in properties) and (BACKEND_CHAIN_BIND_PW in properties) and (BACKEND_CHAIN_URLS in properties)
        if chained_suffix:
            self.log.info("Creating a chaining backend")
            dnbase = DN_CHAIN
        else:
            self.log.info("Creating a local backend")
            dnbase = DN_LDBM
        
        # Get the future backend name
        if properties and BACKEND_NAME in properties:
            cn = properties[BACKEND_NAME]
        else:
            cn = _getBackendName(dnbase)
        
        # Check the future backend name does not already exists
        # we can imagine having no backends for 'suffix' but having a backend 
        # with the same name
        dn = "%s=%s,%s" % (BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_NAME], cn, dnbase)
        ents = self.conn.backend.list(backend_dn=dn)
        if ents:
            raise InvalidArgumentError("It already exists a backend with that DN: %s" % ents[0].dn)
        
        # All checks are done, Time to create the backend
        try:
            entry = Entry(dn)
            entry.update({
                'objectclass': ['top', 'extensibleObject', BACKEND_OBJECTCLASS_VALUE],
                BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_NAME]: cn,
                BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_SUFFIX]: nsuffix
            })
            
            if chained_suffix:
                entry.update({
                             BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_CHAIN_URLS]:    properties[BACKEND_CHAIN_URLS],
                             BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_CHAIN_BIND_DN]: properties[BACKEND_CHAIN_BIND_DN],
                             BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_CHAIN_BIND_PW]: properties[BACKEND_CHAIN_BIND_PW]
                             })

            self.log.debug("adding entry: %r" % entry)
            self.conn.add_s(entry)
        except ldap.ALREADY_EXISTS, e:
            self.log.error("Entry already exists: %r" % dn)
            raise ldap.ALREADY_EXISTS("%s : %r" % (e, dn))
Exemple #18
0
    def delete(self, suffix=None, backend_dn=None, bename=None):
        """
        Deletes the backend entry with the following steps:

        Delete the indexes entries under this backend
        Delete the encrypted attributes entries under this backend
        Delete the encrypted attributes keys entries under this backend 

        If a mapping tree entry uses this backend (nsslapd-backend), it raise UnwillingToPerformError
        
        If 'suffix'/'backend_dn'/'benamebase' are specified. 
        It uses 'backend_dn' first, then 'suffix', then 'benamebase'.
        
        If neither 'suffix', 'backend_dn' and 'benamebase' are specified, it raise InvalidArgumentError 
        @param suffix - suffix of the backend
        @param backend_dn - DN of the backend entry
        @param bename - 'commonname'/'cn' of the backend (e.g. 'userRoot')
        
        @return None
        
        @raise InvalidArgumentError - if missing arguments or invalid
                UnwillingToPerformError - if several backends match the argument
                                     provided suffix does not match backend suffix
                                     It exists a mapping tree that use that backend
        
        
        """
        
        # First check the backend exists and retrieved its suffix
        be_ents = self.conn.backend.list(suffix=suffix, backend_dn=backend_dn, bename=bename)
        if len(be_ents) == 0:
            raise InvalidArgumentError("Unable to retrieve the backend (%r, %r, %r)" % (suffix, backend_dn, bename))
        elif len(be_ents) > 1:
            for ent in be_ents:
                self.log.fatal("Multiple backend match the definition: %s" % ent.dn)
            if (not suffix) and (not backend_dn) and (not bename):
                raise InvalidArgumentError("suffix and backend DN and backend name are missing")
            raise UnwillingToPerformError("Not able to identify the backend to delete")
        else:
            be_ent = be_ents[0]
            be_suffix = be_ent.getValue(BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_SUFFIX])
            
        # Verify the provided suffix is the one stored in the found backend
        if suffix:
            if normalizeDN(suffix) != normalizeDN(be_suffix):
                raise UnwillingToPerformError("provided suffix (%s) differs from backend suffix (%s)" % (suffix, be_suffix))
        
        # now check there is no mapping tree for that suffix
        mt_ents = self.conn.mappingtree.list(suffix=be_suffix)
        if len(mt_ents) > 0:
            raise UnwillingToPerformError("It still exists a mapping tree (%s) for that backend (%s)" % (mt_ents[0].dn, be_ent.dn))
            
        # Now delete the indexes
        found_bename = be_ent.getValue(BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_NAME])
        if not bename:
            bename = found_bename
        elif bename != found_bename:
            raise UnwillingToPerformError("Backend name specified (%s) differs from the retrieved one (%s)" % (bename, found_bename))
        
        self.conn.index.delete_all(bename)
        
        # finally delete the backend children and the backend itself
        ents = self.conn.search_s(be_ent.dn, ldap.SCOPE_ONELEVEL)
        for ent in ents:
            self.log.debug("Delete entry children %s" % (ent.dn))
            self.conn.delete_s(ent.dn)
            
        self.log.debug("Delete backend entry %s" % (be_ent.dn))
        self.conn.delete_s(be_ent.dn)
            
        return
Exemple #19
0
    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: The suffix targeted by the total update
        :type suffix: str
        :param consumer_host: Hostname of the consumer
        :type consumer_host: str
        :param consumer_port: Port of the consumer
        :type consumer_port: int
        :param agmtdn: DN of the replica agreement
        :type agmtdn: str

        :returns: Search result of the replica agreements
        :raises: - InvalidArgument - if missing mandatory argument
                    (agmtdn or suffix, then host and port)
                  - ValueError - if some properties are not valid
                  - NoSuchEntryError - If no replica defined for the suffix
        """

        if not suffix and not agmtdn:
            raise InvalidArgumentError("suffix or agmtdn are required")

        if (consumer_host and not consumer_port) or (not consumer_host
                                                     and consumer_port):
            raise InvalidArgumentError(
                "consumer host/port are required together")

        if agmtdn:
            # easy case, just return the RA
            filt = "objectclass=*"
            return self.conn.search_s(agmtdn, ldap.SCOPE_BASE, filt)
        else:
            # Retrieve the replica
            replica_entries = self.conn.replica.list(suffix)
            if not replica_entries:
                raise NoSuchEntryError("Error: no replica set up for suffix "
                                       "(%s)" % suffix)
            replica_entry = replica_entries[0]

            # Now returns the replica agreement for that suffix that replicates
            # to consumer host/port
            if consumer_host and consumer_port:
                '''
                Currently python does not like long continuous lines when it
                comes to string formatting, so we need to separate each line
                like this.
                '''
                filt = "(&(|(objectclass=%s)" % RA_OBJECTCLASS_VALUE
                filt += "(objectclass=%s))" % RA_WINDOWS_OBJECTCLASS_VALUE
                filt += "(%s=%s)" % (RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_HOST],
                                     consumer_host)
                filt += "(%s=%d))" % (
                    RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_PORT], consumer_port)
            else:
                filt = ("(|(objectclass=%s)(objectclass=%s))" %
                        (RA_OBJECTCLASS_VALUE, RA_WINDOWS_OBJECTCLASS_VALUE))

            return self.conn.search_s(replica_entry.dn, ldap.SCOPE_ONELEVEL,
                                      filt)
Exemple #20
0
    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_PRECISE_PURGING
                REPLICA_REFERRAL
                REPLICA_FLAGS

            @return None

            @raise ValueError: if unknown properties
                    ValueError: if invalid replica_entry
                    ValueError: if replica_dn or suffix are not associated to a replica
                    InvalidArgumentError: If missing mandatory parameter

        '''

        # No properties provided
        if len(properties) == 0:
            return

        # check that the given properties are valid
        for prop in properties:
            # skip the prefix to add/del value
            if not inProperties(prop, REPLICA_PROPNAME_TO_ATTRNAME):
                raise ValueError("unknown property: %s" % prop)
            else:
                self.log.debug("setProperties: %s:%s" %
                               (prop, properties[prop]))

        # At least we need to have suffix/replica_dn/replica_entry
        if not suffix and not replica_dn and not replica_entry:
            raise InvalidArgumentError(
                "suffix and replica_dn and replica_entry are missing")

        # the caller provides a set of properties to set into a replica entry
        if replica_entry:
            if not isinstance(replica_entry, Entry):
                raise ValueError("invalid instance of the replica_entry")

            # that is fine, now set the values
            for prop in properties:
                val = rawProperty(prop)

                # for Entry update it is a replace
                replica_entry.update(
                    {REPLICA_PROPNAME_TO_ATTRNAME[val]: properties[prop]})

            return

        # If it provides the suffix or the replicaDN, replica.list will
        # return the appropriate entry
        ents = self.conn.replica.list(suffix=suffix, replica_dn=replica_dn)
        if len(ents) != 1:
            if replica_dn:
                raise ValueError("invalid replica DN: %s" % replica_dn)
            else:
                raise ValueError("invalid suffix: %s" % suffix)

        # build the MODS
        mods = []
        for prop in properties:
            # take the operation type from the property name
            val = rawProperty(prop)
            if str(prop).startswith('+'):
                op = ldap.MOD_ADD
            elif str(prop).startswith('-'):
                op = ldap.MOD_DELETE
            else:
                op = ldap.MOD_REPLACE

            mods.append(
                (op, REPLICA_PROPNAME_TO_ATTRNAME[val], properties[prop]))

        # that is fine now to apply the MOD
        self.conn.modify_s(ents[0].dn, mods)
Exemple #21
0
    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: The suffix targeted by the total update [mandatory]
        :type suffix: str
        :param consumer_host: Hostname of the consumer [mandatory]
        :type consumer_host: str
        :param consumer_port: Port of the consumer [mandatory]
        :type consumer_port: int

        :returns: None
        :raises: InvalidArgument - if missing mandatory argument
        """

        #
        # 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:
            '''
            Currently python does not like long continuous lines when it
            comes to string formatting, so we need to separate each line
            like this.
            '''
            filt = "(&(objectclass=nsds5replicationagreement)"
            filt += "(nsds5replicahost=%s)" % consumer_host
            filt += "(nsds5replicaport=%d)" % consumer_port
            filt += "(nsds5replicaroot=%s))" % nsuffix

            entry = self.conn.getEntry(replica_entry.dn, ldap.SCOPE_ONELEVEL,
                                       filt)
        except ldap.NO_SUCH_OBJECT:
            msg = ('initAgreement: No replica agreement to ' +
                   '{host}:{port} for suffix {suffix}'.format(
                       host=consumer_host, port=consumer_port, suffix=nsuffix))
            self.log.fatal(msg)
            raise

        #
        # trigger the total init
        #
        self.log.info("Starting total init %s" % entry.dn)
        mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh',
                ensure_bytes('start'))]
        self.conn.modify_s(entry.dn, mod)
Exemple #22
0
    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_PRECISE_PURGING
                    REPLICA_REFERRAL
                    REPLICA_FLAGS

            @return replica DN

            @raise InvalidArgumentError - if missing mandatory arguments
                   ValueError - argument with invalid value

        """
        # Check validity of role
        if not role:
            self.log.fatal(
                "Replica.create: replica role not specify (REPLICAROLE_*)")
            raise InvalidArgumentError("role missing")

        if not Replica._valid_role(role):
            self.log.fatal("enableReplication: replica role invalid (%s) " %
                           role)
            raise ValueError("invalid role: %s" % role)

        # check the validity of 'rid'
        if not Replica._valid_rid(role, rid=rid):
            self.log.fatal(
                "Replica.create: replica role is master but 'rid' is missing or invalid value"
            )
            raise InvalidArgumentError("rid missing or invalid value")

        # check the validity of the suffix
        if not suffix:
            self.log.fatal("Replica.create: suffix is missing")
            raise InvalidArgumentError("suffix missing")
        else:
            nsuffix = normalizeDN(suffix)

        # role is fine, set the replica type
        if role == REPLICAROLE_MASTER:
            rtype = REPLICA_RDWR_TYPE
        else:
            rtype = REPLICA_RDONLY_TYPE

        # Set the properties provided as mandatory parameter
        # The attribute name is not prefixed '+'/'-' => ldap.MOD_REPLACE
        properties = {
            REPLICA_SUFFIX: nsuffix,
            REPLICA_ID: str(rid),
            REPLICA_TYPE: str(rtype)
        }

        # If the properties in args are valid
        # add them to the 'properties' dictionary
        # The attribute name may be prefixed '+'/'-' => keep MOD type as provided in args
        if args:
            for prop in args:
                if not inProperties(prop, REPLICA_PROPNAME_TO_ATTRNAME):
                    raise ValueError("unknown property: %s" % prop)
                properties[prop] = args[prop]

        # Now set default values of unset properties
        Replica._set_or_default(REPLICA_LEGACY_CONS, properties, 'off')
        Replica._set_or_default(REPLICA_BINDDN, properties,
                                [defaultProperties[REPLICATION_BIND_DN]])

        if role != REPLICAROLE_CONSUMER:
            properties[REPLICA_FLAGS] = "1"

        # create replica entry in mapping-tree
        mtents = self.conn.mappingtree.list(suffix=nsuffix)
        mtent = mtents[0]
        dn_replica = ','.join((RDN_REPLICA, mtent.dn))
        try:
            entry = self.conn.getEntry(dn_replica, ldap.SCOPE_BASE)
            self.log.warn("Already setup replica for suffix %r" % nsuffix)
            self.conn.suffixes.setdefault(nsuffix, {})
            self.conn.replica.setProperties(replica_dn=dn_replica,
                                            properties=properties)
            return dn_replica
        except ldap.NO_SUCH_OBJECT:
            entry = None

        #
        # Now create the replica entry
        #
        entry = Entry(dn_replica)
        entry.setValues("objectclass", "top", REPLICA_OBJECTCLASS_VALUE,
                        "extensibleobject")
        self.conn.replica.setProperties(replica_entry=entry,
                                        properties=properties)
        self.conn.add_s(entry)

        # check if the entry exists TODO better to raise!
        self.conn._test_entry(dn_replica, ldap.SCOPE_BASE)

        self.conn.suffixes[nsuffix] = {'dn': dn_replica, 'type': rtype}
        return dn_replica