From 61bc5efd6ae816d6f0ac5a948c5a4ca463b236d5 Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Fri, 21 Jul 2017 17:32:46 +0200 Subject: [PATCH] Issue 79 - Fix replica.py and add tests Description: Move tests for ReplicaLegacy to replicaLegacy_test.py module. Add a new test suite for Replicas(DSLdapObject). Fix or change: - Add enum34 to pip and python-enum34 to specfile for RHEL7/Fedora - _constants.py: - replace REPLICAROLE_* with ReplicaRole(Enum) object - change the type of REPLICA_FLAGS_* from str to int - remove not used constants REPLICA_TYPE_* and REPLICA_FLAGS_CON - Agreement.create() - if property is None, define the dict object; - Changelog.list() - make it consistent and return empry list if nothing is found; - Replica: - Replace hard coded values with variables if possible; - Fix get_role() functions so it will return right result; - Fix deleteAgreements. It now doesn't have suffix param; - Fix typos on variables; - Make promote() and demote() methods more explicit; - Rename Replicas.delete() to Replicas.disable because it coveres changelog and agreements deletion too; - Make docstring format consistent; https://pagure.io/lib389/issue/79 Reviewed by: ? --- lib389/__init__.py | 6 +- lib389/_constants.py | 16 +- lib389/changelog.py | 12 +- lib389/replica.py | 511 +++++++++++++-------------- lib389/repltools.py | 6 +- lib389/tests/agreement_test.py | 4 +- lib389/tests/replicaLegacy_test.py | 459 ++++++++++++++++++++++++ lib389/tests/replica_test.py | 702 ++++++++++++++++--------------------- lib389/topologies.py | 53 +-- lib389/utils.py | 25 +- python-lib389.spec | 1 + requirements.txt | 3 +- 12 files changed, 1077 insertions(+), 721 deletions(-) create mode 100644 lib389/tests/replicaLegacy_test.py diff --git a/lib389/__init__.py b/lib389/__init__.py index 43ead6b..4f6dafb 100644 --- a/lib389/__init__.py +++ b/lib389/__init__.py @@ -2368,11 +2368,11 @@ class DirSrv(SimpleLDAPObject, object): # setup replica # map old style args to new style replica args if repArgs['type'] == MASTER_TYPE: - repArgs['role'] = REPLICAROLE_MASTER + repArgs['role'] = ReplicaRole.MASTER elif repArgs['type'] == LEAF_TYPE: - repArgs['role'] = REPLICAROLE_CONSUMER + repArgs['role'] = ReplicaRole.CONSUMER else: - repArgs['role'] = REPLICAROLE_HUB + repArgs['role'] = ReplicaRole.HUB repArgs['rid'] = repArgs['id'] # remove invalid arguments from replica.add diff --git a/lib389/_constants.py b/lib389/_constants.py index 7e18553..4ca9d0c 100644 --- a/lib389/_constants.py +++ b/lib389/_constants.py @@ -7,6 +7,7 @@ # --- END COPYRIGHT BLOCK --- import os +from enum import Enum from lib389.properties import * ( @@ -17,24 +18,19 @@ from lib389.properties import * INSTALL_LATEST_CONFIG = '999999999' -ROLE_STANDALONE = "standalone" -REPLICAROLE_MASTER = "master" -REPLICAROLE_HUB = "hub" -REPLICAROLE_CONSUMER = "consumer" - REPLICA_FLAGS_CON = 0 +# The structure is convenient for replica promote/demote methods +ReplicaRole = Enum("Replica role", "CONSUMER HUB MASTER STANDALONE") + 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_FLAGS_RDONLY = '0' -REPLICA_FLAGS_WRITE = '1' - -REPLICA_TYPE_MASTER = '3' -REPLICA_TYPE_HUBCON = '2' +REPLICA_FLAGS_RDONLY = 0 +REPLICA_FLAGS_WRITE = 1 REPLICA_RUV_UUID = "ffffffff-ffffffff-ffffffff-ffffffff" REPLICA_RUV_FILTER = ('(&(nsuniqueid=ffffffff-ffffffff-ffffffff-ffffffff)(objectclass=nstombstone))') diff --git a/lib389/changelog.py b/lib389/changelog.py index 66ced8c..5affcee 100644 --- a/lib389/changelog.py +++ b/lib389/changelog.py @@ -26,15 +26,17 @@ class Changelog(object): 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") - + def list(self, suffix=None, changelogdn=DN_CHANGELOG): base = changelogdn filtr = "(objectclass=extensibleobject)" # now do the effective search - ents = self.conn.search_s(base, ldap.SCOPE_BASE, filtr) + try: + ents = self.conn.search_s(base, ldap.SCOPE_BASE, filtr) + except ldap.NO_SUCH_OBJECT: + # There are no objects to select from, se we return an empty array + # as we do in DSLdapObjects + ents = [] return ents def create(self, dbname=DEFAULT_CHANGELOG_DB): diff --git a/lib389/replica.py b/lib389/replica.py index 4de88be..9251ecd 100644 --- a/lib389/replica.py +++ b/lib389/replica.py @@ -18,9 +18,6 @@ from lib389.repltools import ReplTools from lib389 import DirSrv, Entry, NoSuchEntryError, InvalidArgumentError from lib389._mapped_object import DSLdapObjects, DSLdapObject -ROLE_ORDER = {'master': 3, 'hub': 2, 'consumer': 1} -ROLE_TO_NAME = {3: 'master', 2: 'hub', 1: 'consumer'} - class ReplicaLegacy(object): proxied_methods = 'search_s getEntry'.split() @@ -41,16 +38,16 @@ class ReplicaLegacy(object): @staticmethod def _valid_role(role): - if role != REPLICAROLE_MASTER and \ - role != REPLICAROLE_HUB and \ - role != REPLICAROLE_CONSUMER: + 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 role == ReplicaRole.MASTER: if not decimal.Decimal(rid) or \ (rid <= 0) or \ (rid >= CONSUMER_REPLICAID): @@ -257,17 +254,45 @@ class ReplicaLegacy(object): properties=None): raise NotImplementedError + def get_role(self, suffix): + """Return the replica role + + @return: ReplicaRole.MASTER, ReplicaRole.HUB, ReplicaRole.CONSUMER + """ + + filter_str = ('(&(objectclass=nsDS5Replica)(nsDS5ReplicaRoot=%s))'.format(suffix)) + + try: + replica_entry = self.conn.search_s(DN_CONFIG, ldap.SCOPE_SUBTREE, + filter_str) + if replica_entry: + repltype = replica_entry[0].getValue(REPL_TYPE) + replflags = replica_entry[0].getValue(REPL_FLAGS) + + if repltype == REPLICA_RDWR_TYPE and replflags == REPLICA_FLAGS_WRITE: + replicarole = ReplicaRole.MASTER + elif repltype == REPLICA_RDONLY_TYPE and replflags == REPLICA_FLAGS_WRITE: + replicarole = ReplicaRole.HUB + elif repltype == REPLICA_RDONLY_TYPE and replflags == REPLICA_FLAGS_RDONLY: + replicarole = ReplicaRole.CONSUMER + else: + raise ValueError("Failed to determine a replica role") + + return replicarole + except ldap.LDAPError as e: + raise ValueError('Failed to get replica entry: %s' % str(e)) + 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 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. + (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 - dictionary of initial replica's properties @@ -291,13 +316,11 @@ class ReplicaLegacy(object): """ # Check validity of role if not role: - self.log.fatal("Replica.create: replica role not specify" + - " (REPLICAROLE_*)") + self.log.fatal("Replica.create: replica role is not specified (ReplicaRole.*)") raise InvalidArgumentError("role missing") if not ReplicaLegacy._valid_role(role): - self.log.fatal("enableReplication: replica role invalid (%s) " % - role) + self.log.fatal("enableReplication: replica role invalid (%s) " % role) raise ValueError("invalid role: %s" % role) # check the validity of 'rid' @@ -314,7 +337,7 @@ class ReplicaLegacy(object): nsuffix = normalizeDN(suffix) # role is fine, set the replica type - if role == REPLICAROLE_MASTER: + if role == ReplicaRole.MASTER: rtype = REPLICA_RDWR_TYPE else: rtype = REPLICA_RDONLY_TYPE @@ -340,8 +363,11 @@ class ReplicaLegacy(object): ReplicaLegacy._set_or_default(REPLICA_BINDDN, properties, [defaultProperties[REPLICATION_BIND_DN]]) - if role != REPLICAROLE_CONSUMER: - properties[REPLICA_FLAGS] = "1" + # Set flags explicitly, so it will be more readable + if role == ReplicaRole.CONSUMER: + properties[REPLICA_FLAGS] = str(REPLICA_FLAGS_RDONLY) + else: + properties[REPLICA_FLAGS] = str(REPLICA_FLAGS_WRITE) # # Check if replica entry is already in the mapping-tree @@ -397,7 +423,7 @@ class ReplicaLegacy(object): except ldap.LDAPError as e: self.log.fatal('Failed to delete replica agreement (%s),' + ' error: %s' % - (admt.dn, str(e))) + (agmt.dn, str(e))) raise except ldap.LDAPError as e: self.log.fatal('Failed to search for replication agreements ' + @@ -407,7 +433,7 @@ class ReplicaLegacy(object): def disableReplication(self, suffix=None): ''' Delete a replica related to the provided suffix. - If this replica role was REPLICAROLE_HUB or REPLICAROLE_MASTER, it + 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. @@ -455,7 +481,7 @@ class ReplicaLegacy(object): if not role: self.log.fatal("enableReplication: replica role not specify " + - "(REPLICAROLE_*)") + "(ReplicaRole.*)") raise ValueError("role missing") # @@ -463,14 +489,14 @@ class ReplicaLegacy(object): # # First role and replicaID - if role != REPLICAROLE_MASTER and \ - role != REPLICAROLE_HUB and \ - role != REPLICAROLE_CONSUMER: + 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) - if role == REPLICAROLE_MASTER: + if role == ReplicaRole.MASTER: # check the replicaId [1..CONSUMER_REPLICAID[ if not decimal.Decimal(replicaId) or \ (replicaId <= 0) or \ @@ -517,7 +543,7 @@ class ReplicaLegacy(object): pass # First add the changelog if master/hub - if (role == REPLICAROLE_MASTER) or (role == REPLICAROLE_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 @@ -648,50 +674,34 @@ class ReplicaLegacy(object): @raise ValueError """ - if newrole != REPLICAROLE_MASTER and newrole != REPLICAROLE_HUB: + if newrole != ReplicaRole.MASTER and newrole != ReplicaRole.HUB: raise ValueError('Can only prompt replica to "master" or "hub"') if not binddn: raise ValueError('"binddn" required for promotion') - if newrole == REPLICAROLE_MASTER: + if newrole == ReplicaRole.MASTER: if not rid: raise ValueError('"rid" required for promotion') else: # Must be a hub - set the rid rid = CONSUMER_REPLICAID - # - # Get the replica entry - # + # Get replica entry filter_str = ('(&(objectclass=nsDS5Replica)(nsDS5ReplicaRoot=%s))' % suffix) try: - replica_entry = self.conn.search_s('cn=config', ldap.SCOPE_SUBTREE, + replica_entry = self.conn.search_s(DN_CONFIG, ldap.SCOPE_SUBTREE, filter_str) - if replica_entry: - repltype = replica_entry[0].getValue(REPL_TYPE) - replflags = replica_entry[0].getValue(REPL_FLAGS) - - if repltype == REPLICA_TYPE_MASTER and \ - replflags == REPLICA_FLAGS_WRITE: - replicarole = 3 - elif (repltype == REPLICA_TYPE_HUBCON and - replflags == REPLICA_TYPE_MASTER): - replicarole = 2 - else: - replicarole = 1 - - if ROLE_ORDER[newrole] < replicarole: - raise ValueError('Can not promote replica to lower role:' + - ' %s -> %s' % (ROLE_TO_NAME[replicarole], - newrole)) - else: - raise ValueError('Failed to find replica') - except ldap.LDAPError as e: raise ValueError('Failed to get replica entry: %s' % str(e)) + # Check the role type + replicarole = self.get_role(suffix) + + if newrole.value < replicarole.value: + raise ValueError('Can not promote replica to lower role: {} -> {}'.format(replicarole.name, newrole.name)) + # # Create the changelog # @@ -703,7 +713,7 @@ class ReplicaLegacy(object): # # Check that a RID was provided, and its a valid number # - if newrole == REPLICAROLE_MASTER: + if newrole == ReplicaRole.MASTER: try: rid = int(rid) except: @@ -726,7 +736,7 @@ class ReplicaLegacy(object): # # Set the replica type and flags # - if newrole == REPLICAROLE_HUB: + if newrole == ReplicaRole.HUB: try: self.conn.modify_s(replica_entry[0].dn, [(ldap.MOD_REPLACE, REPL_TYPE, '2'), @@ -748,44 +758,28 @@ class ReplicaLegacy(object): @raise ValueError """ - if newrole != REPLICAROLE_CONSUMER and newrole != REPLICAROLE_HUB: + if newrole != ReplicaRole.CONSUMER and newrole != ReplicaRole.HUB: raise ValueError('Can only demote replica to "hub" or "consumer"') - # - # Get the replica entry, and check the role type - # + # Get replica entry filter_str = ('(&(objectclass=nsDS5Replica)(nsDS5ReplicaRoot=%s))' % suffix) try: - replica_entry = self.conn.search_s('cn=config', ldap.SCOPE_SUBTREE, + replica_entry = self.conn.search_s(DN_CONFIG, ldap.SCOPE_SUBTREE, filter_str) - if replica_entry: - repltype = replica_entry[0].getValue(REPL_TYPE) - replflags = replica_entry[0].getValue(REPL_FLAGS) - - if repltype == REPLICA_TYPE_MASTER and \ - replflags == REPLICA_FLAGS_WRITE: - replicarole = 3 - elif (repltype == REPLICA_TYPE_HUBCON and - replflags == REPLICA_FLAGS_WRITE): - replicarole = 2 - else: - replicarole = 1 - - if ROLE_ORDER[newrole] > replicarole: - raise ValueError('Can not demote replica to lower role:' + - ' %s -> %s' % (ROLE_TO_NAME[replicarole], - newrole)) - else: - raise ValueError('Failed to find replica entry') - except ldap.LDAPError as e: raise ValueError('Failed to get replica entry: %s' % str(e)) + # Check the role type + replicarole = self.get_role(suffix) + + if newrole.value > replicarole.value: + raise ValueError('Can not demote replica to higher role: {} -> {}'.format(replicarole.name, newrole.name)) + # # Demote it - set the replica type and flags # - if newrole == 'hub': + if newrole == ReplicaRole.HUB: flag = '1' else: flag = '0' @@ -800,14 +794,16 @@ class ReplicaLegacy(object): class Replica(DSLdapObject): - """Replica object. There is one "replica" per backend - """ + """Replica object. There is one "replica" per backend""" + def __init__(self, instance, dn=None, batch=False): """Init the Replica object + @param instance - a DirSrv object @param dn - A DN of the replica entry @param batch - NOT IMPLELMENTED """ + super(Replica, self).__init__(instance, dn, batch) self._rdn_attribute = 'cn' self._must_attributes = ['cn', REPL_LEGACY_CONS, REPL_TYPE, @@ -825,21 +821,26 @@ class Replica(DSLdapObject): @param role - A string containing the "role" name @return - True if the role is a valid role name, otherwise return False """ - if role != REPLICAROLE_MASTER and \ - role != REPLICAROLE_HUB and \ - role != REPLICAROLE_CONSUMER: + + if role != ReplicaRole.MASTER and \ + role != ReplicaRole.HUB and \ + role != ReplicaRole.CONSUMER: return False else: return True @staticmethod def _valid_rid(role, rid=None): - """ Return True if rid is valid for the replica role + """Return True if rid is valid for the replica role + @param role - A string containing the role name @param rid - Only needed if the role is a "master" @return - True is rid is valid, otherwise return False """ - if role == REPLICAROLE_MASTER: + + if rid is None: + return False + if role == ReplicaRole.MASTER: if not decimal.Decimal(rid) or \ (rid <= 0) or \ (rid >= CONSUMER_REPLICAID): @@ -850,18 +851,17 @@ class Replica(DSLdapObject): return True def delete(self): - ''' - 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. + """Delete a replica related to the provided suffix. - @return None - @raise InvalidArgumentError - if suffix is missing - ldap.LDAPError - for all other update failures + 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. - ''' + @return None + @raise InvalidArgumentError - if suffix is missing + ldap.LDAPError - for all other update failures + """ # Get the suffix suffix = self.get_attr_val(REPL_ROOT) @@ -871,7 +871,7 @@ class Replica(DSLdapObject): # Delete the agreements try: - self.deleteAgreements(suffix) + self.deleteAgreements() except ldap.LDAPError as e: self.log.fatal('Failed to delete replica agreements!') raise e @@ -881,14 +881,14 @@ class Replica(DSLdapObject): super(Replica, self).delete() except ldap.LDAPError as e: self.log.fatal('Failed to delete replica configuration ' + - '(%s), error: %s' % (dn_replica, str(e))) + '(%s), error: %s' % (self._dn, str(e))) raise e def deleteAgreements(self): - ''' - Delete all the agreements for the suffix + """Delete all the agreements for the suffix + @raise LDAPError - If failing to delete or search for agreements - ''' + """ # Delete the agreements try: @@ -899,8 +899,7 @@ class Replica(DSLdapObject): self._instance.delete_s(agmt.dn) except ldap.LDAPError as e: self.log.fatal('Failed to delete replica agreement (%s),' + - ' error: %s' % - (admt.dn, str(e))) + ' error: %s' % (agmt.dn, str(e))) raise e except ldap.LDAPError as e: self.log.fatal('Failed to search for replication agreements ' + @@ -908,13 +907,11 @@ class Replica(DSLdapObject): raise e def promote(self, newrole, binddn=None, rid=None): - """ - Promote the replica + """Promote the replica to hub or master @param newrole - The new replication role for the replica: - REPLICAROLE_MASTER - REPLICAROLE_HUB - REPLICAROLE_CONSUMER + ReplicaRole.MASTER + ReplicaRole.HUB @param binddn - The replication bind dn - only applied to master @param rid - The replication ID, applies only to promotions to "master" @@ -923,51 +920,29 @@ class Replica(DSLdapObject): @raise ValueError """ - if newrole != REPLICAROLE_MASTER and newrole != REPLICAROLE_HUB: - raise ValueError('Can only prompt replica to "master" or "hub"') - if not binddn: - raise ValueError('"binddn" required for promotion') + binddn = defaultProperties[REPLICATION_BIND_DN] - if newrole == REPLICAROLE_MASTER: + # Check the role type + replicarole = self.get_role() + if newrole.value <= replicarole.value: + raise ValueError('Can not promote replica to lower or the same role: {} -> {}'.format(replicarole.name, newrole.name)) + + if newrole == ReplicaRole.MASTER: if not rid: raise ValueError('"rid" required for promotion') else: # Must be a hub - set the rid rid = CONSUMER_REPLICAID - # - # Check the replica role and flags - # - repltype = self.get_attr_val(REPL_TYPE) - replflags = self.get_attr_val(REPL_FLAGS) - - if repltype == REPLICA_TYPE_MASTER and \ - replflags == REPLICA_FLAGS_WRITE: - replicarole = 3 - elif (repltype == REPLICA_TYPE_HUBCON and - replflags == REPLICA_TYPE_MASTER): - replicarole = 2 - else: - replicarole = 1 - - if ROLE_ORDER[newrole] < replicarole: - raise ValueError('Can not promote replica to lower role:' + - ' %s -> %s' % (ROLE_TO_NAME[replicarole], - newrole)) - - # # Create the changelog - # try: self._instance.changelog.create() except ldap.LDAPError as e: raise ValueError('Failed to create changelog: %s' % str(e)) - # # Check that a RID was provided, and its a valid number - # - if newrole == REPLICAROLE_MASTER: + if newrole == ReplicaRole.MASTER: try: rid = int(rid) except: @@ -978,101 +953,97 @@ class Replica(DSLdapObject): raise ValueError('"rid" value (%d) is not in range ' + ' 1 - 65534' % rid) - # # Set bind dn - # try: self.set(REPL_BINDDN, binddn) except ldap.LDAPError as e: raise ValueError('Failed to update replica: ' + str(e)) - # - # Set the replica type and flags - # - if newrole == REPLICAROLE_HUB: + # Promote it - set the replica type, flags and rid + if replicarole == ReplicaRole.CONSUMER and newrole == ReplicaRole.HUB: try: - self.apply_mods([(REPL_TYPE, '2'), (REPL_FLAGS, '1')]) + self.set(REPL_FLAGS, str(REPLICA_FLAGS_WRITE)) except ldap.LDAPError as e: raise ValueError('Failed to update replica: ' + str(e)) - else: # master + elif replicarole == ReplicaRole.CONSUMER and newrole == ReplicaRole.MASTER: try: - self.apply_mods([(REPL_TYPE, '3'), (REPL_FLAGS, '1'), - (REPL_ID, str(rid))]) + self.apply_mods([(REPL_TYPE, str(REPLICA_RDWR_TYPE)), + (REPL_FLAGS, str(REPLICA_FLAGS_WRITE)), + (REPL_ID, str(rid))]) + except ldap.LDAPError as e: + raise ValueError('Failed to update replica: ' + str(e)) + elif replicarole == ReplicaRole.HUB and newrole == ReplicaRole.MASTER: + try: + self.apply_mods([(REPL_TYPE, str(REPLICA_RDWR_TYPE)), + (REPL_ID, str(rid))]) except ldap.LDAPError as e: raise ValueError('Failed to update replica: ' + str(e)) def demote(self, newrole): - """ - Demote a replica to a hub or consumer + """Demote a replica to a hub or consumer + @param suffix - The replication suffix @param newrole - The new replication role of this replica - REPLICAROLE_HUB - REPLICAROLE_CONSUMER + ReplicaRole.HUB + ReplicaRole.CONSUMER @raise ValueError """ - if newrole != REPLICAROLE_CONSUMER and newrole != REPLICAROLE_HUB: - raise ValueError('Can only demote replica to "hub" or "consumer"') - # # Check the role type - # - repltype = self.get_attr_val(REPL_TYPE) - replflags = self.get_attr_val(REPL_FLAGS) - - if repltype == REPLICA_TYPE_MASTER and \ - replflags == REPLICA_FLAGS_WRITE: - replicarole = 3 - elif (repltype == REPLICA_TYPE_HUBCON and - replflags == REPLICA_FLAGS_WRITE): - replicarole = 2 - else: - replicarole = 1 - - if ROLE_ORDER[newrole] > replicarole: - raise ValueError('Can not demote replica to lower role:' + - ' %s -> %s' % (ROLE_TO_NAME[replicarole], - newrole)) + replicarole = self.get_role() + if newrole.value >= replicarole.value: + raise ValueError('Can not demote replica to higher or the same role: {} -> {}'.format(replicarole.name, newrole.name)) - # - # Demote it - set the replica type and flags - # - if newrole == 'hub': - flag = '1' - else: - flag = '0' - try: - self.apply_mods([(REPL_TYPE, '2'), (REPL_FLAGS, flag), - (REPL_ID, str(CONSUMER_REPLICAID))]) - except ldap.LDAPError as e: - raise ValueError('Failed to update replica: ' + str(e)) + # Demote it - set the replica type, flags and rid + if replicarole == ReplicaRole.MASTER and newrole == ReplicaRole.HUB: + try: + self.apply_mods([(REPL_TYPE, str(REPLICA_RDONLY_TYPE)), + (REPL_ID, str(CONSUMER_REPLICAID))]) + except ldap.LDAPError as e: + raise ValueError('Failed to update replica: ' + str(e)) + elif replicarole == ReplicaRole.MASTER and newrole == ReplicaRole.CONSUMER: + try: + self.apply_mods([(REPL_TYPE, str(REPLICA_RDONLY_TYPE)), + (REPL_FLAGS, str(REPLICA_FLAGS_RDONLY)), + (REPL_ID, str(CONSUMER_REPLICAID))]) + except ldap.LDAPError as e: + raise ValueError('Failed to update replica: ' + str(e)) + elif replicarole == ReplicaRole.HUB and newrole == ReplicaRole.CONSUMER: + try: + self.set(REPL_FLAGS, str(REPLICA_FLAGS_RDONLY)) + except ldap.LDAPError as e: + raise ValueError('Failed to update replica: ' + str(e)) def get_role(self): """Return the replica role: - @return: "master", "hub", or "consumer" + @return: 3 for ReplicaRole.MASTER, 2 for ReplicaRole.HUB, 3 for ReplicaRole.CONSUMER """ - repltype = self.get_attr_val(REPL_TYPE) - replflags = self.get_attr_val(REPL_FLAGS) - - if repltype == REPLICA_TYPE_MASTER and \ - replflags == REPLICA_FLAGS_WRITE: - replicarole = 3 - elif (repltype == REPLICA_TYPE_HUBCON and - replflags == REPLICA_TYPE_MASTER): - replicarole = 2 + + repltype = self.get_attr_val_int(REPL_TYPE) + replflags = self.get_attr_val_int(REPL_FLAGS) + + if repltype == REPLICA_RDWR_TYPE and replflags == REPLICA_FLAGS_WRITE: + replicarole = ReplicaRole.MASTER + elif repltype == REPLICA_RDONLY_TYPE and replflags == REPLICA_FLAGS_WRITE: + replicarole = ReplicaRole.HUB + elif repltype == REPLICA_RDONLY_TYPE and replflags == REPLICA_FLAGS_RDONLY: + replicarole = ReplicaRole.CONSUMER else: - replicarole = 1 + raise ValueError("Failed to determine a replica role") - return ROLE_TO_NAME[replicarole] + return replicarole def check_init(self, agmtdn): """Check that a total update has completed + @returns tuple - first element is done/not done, 2nd is no error/has error @param agmtdn - the agreement dn THIS SHOULD BE IN THE NEW AGREEMENT CLASS """ + done, hasError = False, 0 attrlist = ['cn', 'nsds5BeginReplicaRefresh', @@ -1084,8 +1055,7 @@ class Replica(DSLdapObject): entry = self._instance.getEntry( agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist) except NoSuchEntryError: - self.log.exception("Error reading status from agreement %r" % - agmtdn) + self.log.exception("Error reading status from agreement {}".format(agmtdn)) hasError = 1 else: refresh = entry.nsds5BeginReplicaRefresh @@ -1115,11 +1085,13 @@ class Replica(DSLdapObject): def wait_init(self, agmtdn): """Initialize replication and wait for completion. + @oaram agmtdn - agreement dn @return - 0 if the initialization is complete THIS SHOULD BE IN THE NEW AGREEMENT CLASS """ + done = False haserror = 0 while not done and not haserror: @@ -1134,6 +1106,7 @@ class Replica(DSLdapObject): @return - 0 if successful THIS SHOULD BE IN THE NEW AGREEMENT CLASS """ + rc = self.start_async(agmtdn) if not rc: rc = self.wait_init(agmtdn) @@ -1143,10 +1116,11 @@ class Replica(DSLdapObject): def start_async(self, agmtdn): """Initialize replication without waiting. - @param agmtdn - agreement dn + @param agmtdn - agreement dn - THIS SHOULD BE IN THE NEW AGREEMENT CLASS + THIS SHOULD BE IN THE NEW AGREEMENT CLASS """ + self.log.info("Starting async replication %s" % agmtdn) mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')] self._instance.modify_s(agmtdn, mod) @@ -1157,6 +1131,7 @@ class Replica(DSLdapObject): @raise ValeuError - If suffix is not setup for replication LDAPError - If there is a problem trying to search for the RUV """ + try: entry = self._instance.search_s(self._suffix, ldap.SCOPE_SUBTREE, @@ -1164,20 +1139,19 @@ class Replica(DSLdapObject): if entry: return entry[0] else: - raise ValueError('Suffix (%s) is not setup for replication' % - suffix) + raise ValueError('Suffix (%s) is not setup for replication' % self._suffix) except ldap.LDAPError as e: raise e def test(self, *replica_dirsrvs): - '''Make a "dummy" update on the the replicated suffix, and check - all the provided replicas to see if they received the update. + """Make a "dummy" update on the the replicated suffix, and check + all the provided replicas to see if they received the update. - @param *replica_dirsrvs - DirSrv instance, DirSrv instance, ... - @return True - if all servers have recevioed the update by this - replica, otherwise return False - @raise LDAPError - when failing to update/search database - ''' + @param *replica_dirsrvs - DirSrv instance, DirSrv instance, ... + @return True - if all servers have recevioed the update by this + replica, otherwise return False + @raise LDAPError - when failing to update/search database + """ # Generate a unique test value test_value = ('test replication from ' + self._instance.serverid + @@ -1217,20 +1191,23 @@ class Replica(DSLdapObject): class Replicas(DSLdapObjects): """Class of all the Replicas""" + def __init__(self, instance, batch=False): """Init Replicas + @param instance - a DirSrv objectc @param batch - NOT IMPLELMENTED """ + super(Replicas, self).__init__(instance=instance, batch=False) self._objectclasses = [REPLICA_OBJECTCLASS_VALUE] self._filterattrs = [REPL_ROOT] self._childobject = Replica - self._basedn = 'cn=mapping tree,cn=config' + self._basedn = DN_MAPPING_TREE def get(self, selector=[], dn=None): - """Wrap Replicas' "get", and set the suffix after we get the replica - """ + """Wrap Replicas' "get", and set the suffix after we get the replica """ + replica = super(Replicas, self).get(selector, dn) if replica: # Get and set the replica's suffix @@ -1240,40 +1217,38 @@ class Replicas(DSLdapObjects): def enable(self, suffix, role, replicaID=None, args=None): """Enable replication for this suffix - @param suffix - The suffix to enable replication for - @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 - dictionary of additional replica properties + @param suffix - The suffix to enable replication for + @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. - @return replica DN + @param args - dictionary of additional replica properties - @raise InvalidArgumentError - if missing mandatory arguments - ValueError - argument with invalid value - LDAPError - failed to add replica entry + @return replica DN + @raise InvalidArgumentError - if missing mandatory arguments + ValueError - argument with invalid value + LDAPError - failed to add replica entry """ + # Normalize the suffix suffix = normalizeDN(suffix) # Check validity of role if not role: - self._log.fatal("Replica.create: replica role not specify" + - " (REPLICAROLE_*)") + self._log.fatal("Replica.create: replica role is not specified (ReplicaRole.*)") raise InvalidArgumentError("role missing") if not Replica._valid_role(role): - self._log.fatal("enableReplication: replica role invalid (%s) " % - role) + 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: + if role == ReplicaRole.MASTER: rtype = REPLICA_RDWR_TYPE # check the validity of 'rid' if not Replica._valid_rid(role, rid=replicaID): @@ -1299,24 +1274,24 @@ class Replicas(DSLdapObjects): # Now set default values of unset properties if REPLICA_LEGACY_CONS not in properties: properties[REPL_LEGACY_CONS] = 'off' - if role != REPLICAROLE_CONSUMER: - properties[REPL_FLAGS] = "1" - # + # Set flags explicitly, so it will be more readable + if role == ReplicaRole.CONSUMER: + properties[REPL_FLAGS] = str(REPLICA_FLAGS_RDONLY) + else: + properties[REPL_FLAGS] = str(REPLICA_FLAGS_WRITE) + # Check if replica entry is already in the mapping-tree - # try: replica = self.get(suffix) # Should we return an error, or just return the existing relica? self._log.warn("Already setup replica for suffix %s" % suffix) return replica - except: + except ldap.NO_SUCH_OBJECT: pass - # # Create changelog - # - if (role == REPLICAROLE_MASTER) or (role == REPLICAROLE_HUB): + if (role == ReplicaRole.MASTER) or (role == ReplicaRole.HUB): self._instance.changelog.create() # Create the default replica manager entry if it does not exist @@ -1333,9 +1308,7 @@ class Replicas(DSLdapObjects): repl_manager_dn=properties[REPL_BINDDN], repl_manager_pw=repl_pw) - # # Now create the replica entry - # mtents = self._instance.mappingtree.list(suffix=suffix) suffix_dn = mtents[0].dn replica = self.create(RDN_REPLICA, properties) @@ -1343,17 +1316,22 @@ class Replicas(DSLdapObjects): return replica - def delete(self, suffix): + def disable(self, suffix): """Disable replication on the suffix specified @param suffix - Replicated suffix to disable @raise ValueError is suffix is not being replicated """ + try: replica = self.get(suffix) except ldap.NO_SUCH_OBJECT: - raise ValueError('Suffix (%s) is not setup for replication' % - suffix) + raise ValueError('Suffix (%s) is not setup for replication' % suffix) + + role = replica.get_role() + if role in (ReplicaRole.MASTER, ReplicaRole.HUB): + self._instance.changelog.delete() + try: replica.delete() except ldap.LDAPError as e: @@ -1365,36 +1343,37 @@ class Replicas(DSLdapObjects): @param suffix - The replication suffix @param newrole - The new replication role for the replica: - REPLICAROLE_MASTER - REPLICAROLE_HUB + ReplicaRole.MASTER + ReplicaRole.HUB @param binddn - The replication bind dn - only applied to master @param rid - The replication ID - applies only promotions to "master" @raise ldap.NO_SUCH_OBJECT - If suffix is not replicated """ + replica = self.get(suffix) try: replica = self.get(suffix) except ldap.NO_SUCH_OBJECT: - raise ValueError('Suffix (%s) is not setup for replication' % - suffix) + raise ValueError('Suffix (%s) is not setup for replication' % suffix) replica.promote(newrole, binddn, rid) def demote(self, suffix, newrole): """Promote the replica speficied by the suffix to the new role + @param suffix - The replication suffix @param newrole - The new replication role of this replica - REPLICAROLE_HUB - REPLICAROLE_CONSUMER + ReplicaRole.HUB + ReplicaRole.CONSUMER @raise ldap.NO_SUCH_OBJECT - If suffix is not replicated """ + replica = self.get(suffix) try: replica = self.get(suffix) except ldap.NO_SUCH_OBJECT: - raise ValueError('Suffix (%s) is not setup for replication' % - suffix) + raise ValueError('Suffix (%s) is not setup for replication' % suffix) replica.demote(newrole) def get_dn(self, suffix): @@ -1407,36 +1386,36 @@ class Replicas(DSLdapObjects): try: replica = self.get(suffix) except ldap.NO_SUCH_OBJECT: - raise ValueError('Suffix (%s) is not setup for replication' % - suffix) + raise ValueError('Suffix (%s) is not setup for replication' % suffix) return replica._dn def get_ruv_entry(self, suffix): """Return the database RUV entry for the provided suffix + @return - The database RUV entry @raise ValeuError - If suffix is not setup for replication LDAPError - If there is a problem trying to search for the RUV """ + try: replica = self.get(suffix) except ldap.NO_SUCH_OBJECT: - raise ValueError('Suffix (%s) is not setup for replication' % - suffix) + raise ValueError('Suffix (%s) is not setup for replication' % suffix) return replica.get_ruv_entry() def test(self, suffix, *replica_dirsrvs): """Make a "dummy" update on the the replicated suffix, and check - all the provided replicas to see if they received the update. + all the provided replicas to see if they received the update. - @param suffix - the replicated suffix we want to check - @param *replica_dirsrvs - DirSrv instance, DirSrv instance, ... - @return True - if all servers have recevioed the update by this - replica, otherwise return False - @raise LDAPError - when failing to update/search database + @param suffix - the replicated suffix we want to check + @param *replica_dirsrvs - DirSrv instance, DirSrv instance, ... + @return True - if all servers have recevioed the update by this + replica, otherwise return False + @raise LDAPError - when failing to update/search database """ + try: replica = self.get(suffix) except ldap.NO_SUCH_OBJECT: - raise ValueError('Suffix (%s) is not setup for replication' % - suffix) + raise ValueError('Suffix (%s) is not setup for replication' % suffix) return replica.test(*replica_dirsrvs) diff --git a/lib389/repltools.py b/lib389/repltools.py index d1f4193..67eb5c3 100644 --- a/lib389/repltools.py +++ b/lib389/repltools.py @@ -160,11 +160,11 @@ class ReplTools(object): ctime = _getCSNTime(inst, csnstr) if ctime: role = replObj.get_role() - if role == REPLICAROLE_MASTER: + if role == ReplicaRole.MASTER: txt = ' Master (%s)' % (inst.serverid) - elif role == REPLICAROLE_HUB: + elif role == ReplicaRole.HUB: txt = ' Hub (%s)' % (inst.serverid) - elif role == REPLICAROLE_CONSUMER: + elif role == ReplicaRole.CONSUMER: txt = ' Consumer (%s)' % (inst.serverid) else: txt = '?' diff --git a/lib389/tests/agreement_test.py b/lib389/tests/agreement_test.py index 456cbfa..c39b08c 100644 --- a/lib389/tests/agreement_test.py +++ b/lib389/tests/agreement_test.py @@ -61,7 +61,7 @@ def topology(request): # Enable replication master.replica.enableReplication(suffix=SUFFIX, - role=REPLICAROLE_MASTER, + role=ReplicaRole.MASTER, replicaId=REPLICAID_MASTER) # Consumer @@ -80,7 +80,7 @@ def topology(request): # Enable replication consumer.replica.enableReplication(suffix=SUFFIX, - role=REPLICAROLE_CONSUMER) + role=ReplicaRole.CONSUMER) # Delete each instance in the end def fin(): diff --git a/lib389/tests/replicaLegacy_test.py b/lib389/tests/replicaLegacy_test.py new file mode 100644 index 0000000..670b81d --- /dev/null +++ b/lib389/tests/replicaLegacy_test.py @@ -0,0 +1,459 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2015 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- +# +import ldap +import os +import pytest +import logging + +from lib389 import InvalidArgumentError +from lib389._constants import * +from lib389.properties import * +from lib389 import DirSrv, Entry + +logging.getLogger(__name__).setLevel(logging.DEBUG) +log = logging.getLogger(__name__) + +# Used for One master / One consumer topology +HOST_MASTER = LOCALHOST +PORT_MASTER = 40389 +SERVERID_MASTER = 'master' +REPLICAID_MASTER = 1 + +HOST_CONSUMER = LOCALHOST +PORT_CONSUMER = 50389 +SERVERID_CONSUMER = 'consumer' + +TEST_REPL_DN = "uid=test,%s" % DEFAULT_SUFFIX +INSTANCE_PORT = 54321 +INSTANCE_SERVERID = 'dirsrv' +INSTANCE_BACKUP = os.environ.get('BACKUPDIR', DEFAULT_BACKUPDIR) +NEW_SUFFIX_1 = 'ou=test_master' +NEW_BACKEND_1 = 'test_masterdb' +NEW_RM_1 = "cn=replrepl,%s" % NEW_SUFFIX_1 + +NEW_SUFFIX_2 = 'ou=test_consumer' +NEW_BACKEND_2 = 'test_consumerdb' + +NEW_SUFFIX_3 = 'ou=test_enablereplication_1' +NEW_BACKEND_3 = 'test_enablereplicationdb_1' + +NEW_SUFFIX_4 = 'ou=test_enablereplication_2' +NEW_BACKEND_4 = 'test_enablereplicationdb_2' + +NEW_SUFFIX_5 = 'ou=test_enablereplication_3' +NEW_BACKEND_5 = 'test_enablereplicationdb_3' + + +class TopologyReplication(object): + def __init__(self, master, consumer): + master.open() + consumer.open() + self.master = master + self.consumer = consumer + + +@pytest.fixture(scope="module") +def topology(request): + # Create the master instance + master = DirSrv(verbose=False) + master.log.debug("Master allocated") + args = {SER_HOST: HOST_MASTER, + SER_PORT: PORT_MASTER, + SER_SERVERID_PROP: SERVERID_MASTER} + master.allocate(args) + if master.exists(): + master.delete() + master.create() + master.open() + + # Create the consumer instance + consumer = DirSrv(verbose=False) + consumer.log.debug("Consumer allocated") + args = {SER_HOST: HOST_CONSUMER, + SER_PORT: PORT_CONSUMER, + SER_SERVERID_PROP: SERVERID_CONSUMER} + consumer.allocate(args) + if consumer.exists(): + consumer.delete() + consumer.create() + consumer.open() + + # Delete each instance in the end + def fin(): + master.delete() + consumer.delete() + request.addfinalizer(fin) + + return TopologyReplication(master, consumer) + + +def test_create(topology): + """This test creates + - suffix/backend (NEW_SUFFIX_[12], NEW_BACKEND_[12]) : Master + - suffix/backend (NEW_SUFFIX_[12], NEW_BACKEND_[12]) : Consumer + - replica NEW_SUFFIX_1 as MASTER : Master + - replica NEW_SUFFIX_2 as CONSUMER : Master + """ + + log.info("\n\n##########\n### CREATE\n############") + # + # MASTER (suffix/backend) + # + backendEntry = topology.master.backend.create( + suffix=NEW_SUFFIX_1, properties={BACKEND_NAME: NEW_BACKEND_1}) + backendEntry = topology.master.backend.create( + suffix=NEW_SUFFIX_2, properties={BACKEND_NAME: NEW_BACKEND_2}) + + ents = topology.master.mappingtree.list() + master_nb_mappingtree = len(ents) + + # create a first additional mapping tree + topology.master.mappingtree.create(NEW_SUFFIX_1, bename=NEW_BACKEND_1) + ents = topology.master.mappingtree.list() + assert len(ents) == (master_nb_mappingtree + 1) + topology.master.add_s(Entry((NEW_SUFFIX_1, + {'objectclass': "top organizationalunit".split(), + 'ou': NEW_SUFFIX_1.split('=', 1)[1]}))) + + # create a second additional mapping tree + topology.master.mappingtree.create(NEW_SUFFIX_2, bename=NEW_BACKEND_2) + ents = topology.master.mappingtree.list() + assert len(ents) == (master_nb_mappingtree + 2) + topology.master.add_s(Entry((NEW_SUFFIX_2, + {'objectclass': "top organizationalunit".split(), + 'ou': NEW_SUFFIX_2.split('=', 1)[1]}))) + log.info('Master it exists now %d suffix(es)' % len(ents)) + + # + # CONSUMER (suffix/backend) + # + backendEntry = topology.consumer.backend.create( + suffix=NEW_SUFFIX_1, properties={BACKEND_NAME: NEW_BACKEND_1}) + backendEntry = topology.consumer.backend.create( + suffix=NEW_SUFFIX_2, properties={BACKEND_NAME: NEW_BACKEND_2}) + + ents = topology.consumer.mappingtree.list() + consumer_nb_mappingtree = len(ents) + + # create a first additional mapping tree + topology.consumer.mappingtree.create(NEW_SUFFIX_1, bename=NEW_BACKEND_1) + ents = topology.consumer.mappingtree.list() + assert len(ents) == (consumer_nb_mappingtree + 1) + topology.consumer.add_s(Entry((NEW_SUFFIX_1, + {'objectclass': "top organizationalunit".split(), + 'ou': NEW_SUFFIX_1.split('=', 1)[1]}))) + + # create a second additional mapping tree + topology.consumer.mappingtree.create(NEW_SUFFIX_2, bename=NEW_BACKEND_2) + ents = topology.consumer.mappingtree.list() + assert len(ents) == (consumer_nb_mappingtree + 2) + topology.consumer.add_s(Entry((NEW_SUFFIX_2, + {'objectclass': "top organizationalunit".split(), + 'ou': NEW_SUFFIX_2.split('=', 1)[1]}))) + log.info('Consumer it exists now %d suffix(es)' % len(ents)) + + # + # Now create REPLICAS on master + # + # check it exists this entry to stores the changelogs + topology.master.changelog.create() + + # create a master + topology.master.replica.create(suffix=NEW_SUFFIX_1, + role=ReplicaRole.MASTER, + rid=1) + ents = topology.master.replica.list() + assert len(ents) == 1 + log.info('Master replica %s' % ents[0].dn) + + # create a consumer + topology.master.replica.create(suffix=NEW_SUFFIX_2, + role=ReplicaRole.CONSUMER) + ents = topology.master.replica.list() + assert len(ents) == 2 + ents = topology.master.replica.list(suffix=NEW_SUFFIX_2) + log.info('Consumer replica %s' % ents[0].dn) + + # + # Now create REPLICAS on consumer + # + # create a master + topology.consumer.replica.create(suffix=NEW_SUFFIX_1, + role=ReplicaRole.CONSUMER) + ents = topology.consumer.replica.list() + assert len(ents) == 1 + log.info('Consumer replica %s' % ents[0].dn) + + # create a consumer + topology.consumer.replica.create(suffix=NEW_SUFFIX_2, + role=ReplicaRole.CONSUMER) + ents = topology.consumer.replica.list() + assert len(ents) == 2 + ents = topology.consumer.replica.list(suffix=NEW_SUFFIX_2) + log.info('Consumer replica %s' % ents[0].dn) + + +def test_list(topology): + """This test checks: + - existing replicas can be retrieved + - access to unknown replica does not fail + + PRE-CONDITION: + It exists on MASTER two replicas NEW_SUFFIX_1 and NEW_SUFFIX_2 + created by test_create() + """ + + log.info("\n\n############\n### LIST\n############") + ents = topology.master.replica.list() + assert len(ents) == 2 + + # Check we can retrieve a replica with its suffix + ents = topology.master.replica.list(suffix=NEW_SUFFIX_1) + assert len(ents) == 1 + replica_dn_1 = ents[0].dn + + # Check we can retrieve a replica with its suffix + ents = topology.master.replica.list(suffix=NEW_SUFFIX_2) + assert len(ents) == 1 + replica_dn_2 = ents[0].dn + + # Check we can retrieve a replica with its DN + ents = topology.master.replica.list(replica_dn=replica_dn_1) + assert len(ents) == 1 + assert replica_dn_1 == ents[0].dn + + # Check we can retrieve a replica if we provide DN and suffix + ents = topology.master.replica.list(suffix=NEW_SUFFIX_2, + replica_dn=replica_dn_2) + assert len(ents) == 1 + assert replica_dn_2 == ents[0].dn + + # Check DN is used before suffix name + ents = topology.master.replica.list(suffix=NEW_SUFFIX_2, + replica_dn=replica_dn_1) + assert len(ents) == 1 + assert replica_dn_1 == ents[0].dn + + # Check that invalid value does not break + ents = topology.master.replica.list(suffix="X") + for ent in ents: + log.critical("Unexpected replica: %s" % ent.dn) + assert len(ents) == 0 + + +def test_create_repl_manager(topology): + """The tests are + - create the default Replication manager/Password + - create a specific Replication manager/ default Password + - Check we can bind successfully + - create a specific Replication manager / specific Password + - Check we can bind successfully + """ + + log.info("\n\n###########\n### CREATE_REPL_MANAGER\n###########") + # First create the default replication manager + topology.consumer.replica.create_repl_manager() + ents = topology.consumer.search_s(defaultProperties[REPLICATION_BIND_DN], + ldap.SCOPE_BASE, "objectclass=*") + assert len(ents) == 1 + assert ents[0].dn == defaultProperties[REPLICATION_BIND_DN] + + # Second create a custom replication manager under NEW_SUFFIX_2 + rm_dn = "cn=replrepl,%s" % NEW_SUFFIX_2 + topology.consumer.replica.create_repl_manager(repl_manager_dn=rm_dn) + ents = topology.consumer.search_s(rm_dn, ldap.SCOPE_BASE, "objectclass=*") + assert len(ents) == 1 + assert ents[0].dn == rm_dn + + # Check we can bind + topology.consumer.simple_bind_s(rm_dn, + defaultProperties[REPLICATION_BIND_PW]) + + # Check we fail to bind + with pytest.raises(ldap.INVALID_CREDENTIALS) as excinfo: + topology.consumer.simple_bind_s(rm_dn, "dummy") + log.info("Exception: %s" % str(excinfo.value)) + + # now rebind + topology.consumer.simple_bind_s(topology.consumer.binddn, + topology.consumer.bindpw) + + # Create a custom replication manager under NEW_SUFFIX_1 + # with a specified password + rm_dn = NEW_RM_1 + topology.consumer.replica.create_repl_manager(repl_manager_dn=rm_dn, + repl_manager_pw="Secret123") + ents = topology.consumer.search_s(rm_dn, ldap.SCOPE_BASE, "objectclass=*") + assert len(ents) == 1 + assert ents[0].dn == rm_dn + + # Check we can bind + topology.consumer.simple_bind_s(rm_dn, "Secret123") + + # Check we fail to bind + with pytest.raises(ldap.INVALID_CREDENTIALS) as excinfo: + topology.consumer.simple_bind_s(rm_dn, "dummy") + log.info("Exception: %s" % str(excinfo.value)) + topology.consumer.simple_bind_s(topology.consumer.binddn, + topology.consumer.bindpw) + + +def test_enableReplication(topology): + """It checks + - Ability to enable replication on a supplier + - Ability to enable replication on a consumer + - Failure to enable replication with wrong replicaID on supplier + - Failure to enable replication with wrong replicaID on consumer + """ + + log.info("\n\n############\n### ENABLEREPLICATION\n##########") + # + # MASTER (suffix/backend) + # + backendEntry = topology.master.backend.create(suffix=NEW_SUFFIX_3, + properties={BACKEND_NAME: + NEW_BACKEND_3}) + + ents = topology.master.mappingtree.list() + master_nb_mappingtree = len(ents) + + # create a first additional mapping tree + topology.master.mappingtree.create(NEW_SUFFIX_3, bename=NEW_BACKEND_3) + ents = topology.master.mappingtree.list() + assert len(ents) == (master_nb_mappingtree + 1) + topology.master.add_s(Entry((NEW_SUFFIX_3, + {'objectclass': "top organizationalunit".split(), + 'ou': NEW_SUFFIX_3.split('=', 1)[1]}))) + + # a supplier should have replicaId in [1..CONSUMER_REPLICAID[ + with pytest.raises(ValueError) as excinfo: + topology.master.replica.enableReplication(suffix=NEW_SUFFIX_3, + role=ReplicaRole.MASTER, + replicaId=CONSUMER_REPLICAID) + log.info("Exception (expected): %s" % str(excinfo.value)) + topology.master.replica.enableReplication(suffix=NEW_SUFFIX_3, + role=ReplicaRole.MASTER, + replicaId=1) + + # + # MASTER (suffix/backend) + # + backendEntry = topology.master.backend.create(suffix=NEW_SUFFIX_4, + properties={BACKEND_NAME: + NEW_BACKEND_4}) + + ents = topology.master.mappingtree.list() + master_nb_mappingtree = len(ents) + + # create a first additional mapping tree + topology.master.mappingtree.create(NEW_SUFFIX_4, bename=NEW_BACKEND_4) + ents = topology.master.mappingtree.list() + assert len(ents) == (master_nb_mappingtree + 1) + topology.master.add_s(Entry((NEW_SUFFIX_4, + {'objectclass': "top organizationalunit".split(), + 'ou': NEW_SUFFIX_4.split('=', 1)[1]}))) + + # A consumer should have CONSUMER_REPLICAID not '1' + with pytest.raises(ValueError) as excinfo: + topology.master.replica.enableReplication(suffix=NEW_SUFFIX_4, + role=ReplicaRole.CONSUMER, + replicaId=1) + log.info("Exception (expected): %s" % str(excinfo.value)) + topology.master.replica.enableReplication(suffix=NEW_SUFFIX_4, + role=ReplicaRole.CONSUMER) + + +def test_disableReplication(topology): + """It checks + - Ability to disable replication on a supplier + - Ability to disable replication on a consumer + - Failure to disable replication with wrong suffix on supplier + - Failure to disable replication with wrong suffix on consumer + """ + + log.info("\n\n############\n### DISABLEREPLICATION\n##########") + topology.master.replica.disableReplication(suffix=NEW_SUFFIX_3) + with pytest.raises(ldap.LDAPError) as excinfo: + topology.master.replica.disableReplication(suffix=NEW_SUFFIX_3) + log.info("Exception (expected): %s" % str(excinfo.value)) + + topology.master.replica.disableReplication(suffix=NEW_SUFFIX_4) + with pytest.raises(ldap.LDAPError) as excinfo: + topology.master.replica.disableReplication(suffix=NEW_SUFFIX_4) + log.info("Exception (expected): %s" % str(excinfo.value)) + + +def test_setProperties(topology): + """Set some properties + Verified that valid properties are set + Verified that invalid properties raise an Exception + + PRE-REQUISITE: it exists a replica for NEW_SUFFIX_1 + """ + + log.info("\n\n##########\n### SETPROPERTIES\n############") + # set valid values to SUFFIX_1 + properties = {REPLICA_LEGACY_CONS: 'off', + REPLICA_BINDDN: NEW_RM_1, + REPLICA_PURGE_INTERVAL: str(3600), + REPLICA_PURGE_DELAY: str(5 * 24 * 3600), + REPLICA_REFERRAL: "ldap://%s:1234/" % LOCALHOST} + topology.master.replica.setProperties(suffix=NEW_SUFFIX_1, + properties=properties) + + # Check the values have been written + replicas = topology.master.replica.list(suffix=NEW_SUFFIX_1) + assert len(replicas) == 1 + for prop in properties: + attr = REPLICA_PROPNAME_TO_ATTRNAME[prop] + val = replicas[0].getValue(attr) + log.info("Replica[%s] -> %s: %s" % (prop, attr, val)) + assert val == properties[prop] + + # Check invalid properties raise exception + with pytest.raises(ValueError) as excinfo: + properties = {"dummy": 'dummy'} + topology.master.replica.setProperties(suffix=NEW_SUFFIX_1, + properties=properties) + log.info("Exception (expected): %s" % str(excinfo.value)) + + # check call without suffix/dn/entry raise InvalidArgumentError + with pytest.raises(InvalidArgumentError) as excinfo: + properties = {REPLICA_LEGACY_CONS: 'off'} + topology.master.replica.setProperties(properties=properties) + log.info("Exception (expected): %s" % str(excinfo.value)) + + # check that if we do not provide a valid entry it raises ValueError + with pytest.raises(ValueError) as excinfo: + properties = {REPLICA_LEGACY_CONS: 'off'} + topology.master.replica.setProperties(replica_entry="dummy", + properties=properties) + log.info("Exception (expected): %s" % str(excinfo.value)) + + # check that with an invalid suffix or replica_dn it raise ValueError + with pytest.raises(ValueError) as excinfo: + properties = {REPLICA_LEGACY_CONS: 'off'} + topology.master.replica.setProperties(suffix="dummy", + properties=properties) + log.info("Exception (expected): %s" % str(excinfo.value)) + + +def test_getProperties(topology): + """Currently not implemented""" + + log.info("\n\n############\n### GETPROPERTIES\n###########") + with pytest.raises(NotImplementedError) as excinfo: + properties = {REPLICA_LEGACY_CONS: 'off'} + topology.master.replica.getProperties(suffix=NEW_SUFFIX_1) + log.info("Exception (expected): %s" % str(excinfo.value)) + + +if __name__ == "__main__": + CURRENT_FILE = os.path.realpath(__file__) + pytest.main("-s -v %s" % CURRENT_FILE) diff --git a/lib389/tests/replica_test.py b/lib389/tests/replica_test.py index 9eb0078..7250d67 100644 --- a/lib389/tests/replica_test.py +++ b/lib389/tests/replica_test.py @@ -1,457 +1,373 @@ # --- BEGIN COPYRIGHT BLOCK --- -# Copyright (C) 2015 Red Hat, Inc. +# Copyright (C) 2017 Red Hat, Inc. # All rights reserved. # # License: GPL (version 3 or any later version). # See LICENSE for details. # --- END COPYRIGHT BLOCK --- # -import ldap import os +import ldap import pytest import logging -from lib389 import InvalidArgumentError -from lib389._constants import * -from lib389.properties import * -from lib389 import DirSrv, Entry +from lib389 import NoSuchEntryError +from lib389.replica import Replicas +from lib389.backend import Backends +from lib389.idm.domain import Domain +from lib389._constants import (ReplicaRole, BACKEND_SUFFIX, BACKEND_NAME, REPLICA_RUV_FILTER, CONSUMER_REPLICAID, + REPLICA_FLAGS_WRITE, REPLICA_RDWR_TYPE) +from lib389.properties import (REPL_FLAGS, REPL_TYPE) +from lib389.topologies import topology_i3 as topo logging.getLogger(__name__).setLevel(logging.DEBUG) log = logging.getLogger(__name__) -# Used for One master / One consumer topology -HOST_MASTER = LOCALHOST -PORT_MASTER = 40389 -SERVERID_MASTER = 'master' -REPLICAID_MASTER = 1 +DEBUGGING = os.getenv('DEBUGGING', default=False) +NEW_SUFFIX = 'dc=test,dc=com' +NEW_BACKEND = 'test_backend' +REPLICA_MASTER_ID = 1 -HOST_CONSUMER = LOCALHOST -PORT_CONSUMER = 50389 -SERVERID_CONSUMER = 'consumer' -TEST_REPL_DN = "uid=test,%s" % DEFAULT_SUFFIX -INSTANCE_PORT = 54321 -INSTANCE_SERVERID = 'dirsrv' -INSTANCE_BACKUP = os.environ.get('BACKUPDIR', DEFAULT_BACKUPDIR) -NEW_SUFFIX_1 = 'ou=test_master' -NEW_BACKEND_1 = 'test_masterdb' -NEW_RM_1 = "cn=replrepl,%s" % NEW_SUFFIX_1 +@pytest.fixture(scope="module") +def new_suffixes(topo): + """Create new suffix, backend and mapping tree""" + + for num in range(1, 4): + backends = Backends(topo.ins["standalone{}".format(num)]) + backends.create(properties={BACKEND_SUFFIX: NEW_SUFFIX, + BACKEND_NAME: NEW_BACKEND}) + domain = Domain(topo.ins["standalone{}".format(num)], NEW_SUFFIX) + domain.create(properties={'dc': 'test', 'description': NEW_SUFFIX}) + + +@pytest.fixture(scope="function") +def simple_replica(topo, new_suffixes, request): + """Enable simple multi-master replication""" + + master1 = topo.ins["standalone1"] + master2 = topo.ins["standalone2"] + + log.info("Enable two master replicas") + replicas_m1 = Replicas(master1) + replica_m1 = replicas_m1.enable(suffix=NEW_SUFFIX, + role=ReplicaRole.MASTER, + replicaID=REPLICA_MASTER_ID) + replicas_m2 = Replicas(master2) + replica_m2 = replicas_m2.enable(suffix=NEW_SUFFIX, + role=ReplicaRole.MASTER, + replicaID=REPLICA_MASTER_ID+1) + + log.info("Create agreements between the instances") + master1.agreement.create(suffix=NEW_SUFFIX, + host=master2.host, + port=master2.port) + master2.agreement.create(suffix=NEW_SUFFIX, + host=master1.host, + port=master1.port) + + log.info("Test replication") + replicas_m1.test(NEW_SUFFIX, master2) -NEW_SUFFIX_2 = 'ou=test_consumer' -NEW_BACKEND_2 = 'test_consumerdb' + def fin(): + replicas_m1.disable(NEW_SUFFIX) + replicas_m2.disable(NEW_SUFFIX) + request.addfinalizer(fin) -NEW_SUFFIX_3 = 'ou=test_enablereplication_1' -NEW_BACKEND_3 = 'test_enablereplicationdb_1' + return [replica_m1, replica_m2] -NEW_SUFFIX_4 = 'ou=test_enablereplication_2' -NEW_BACKEND_4 = 'test_enablereplicationdb_2' -NEW_SUFFIX_5 = 'ou=test_enablereplication_3' -NEW_BACKEND_5 = 'test_enablereplicationdb_3' +@pytest.fixture(scope="function") +def clean_up(topo, new_suffixes, request): + """Check that all replicas were disabled and disable if not""" + def fin(): + for num in range(1, 4): + try: + replicas = Replicas(topo.ins["standalone{}".format(num)]) + replicas.disable(NEW_SUFFIX) + log.info("standalone{} is disabled now".format(num)) + except: + pass + request.addfinalizer(fin) -class TopologyReplication(object): - def __init__(self, master, consumer): - master.open() - consumer.open() - self.master = master - self.consumer = consumer +def test_delete_agreements(topo, simple_replica): + """Check deleteAgreements method -@pytest.fixture(scope="module") -def topology(request): - # Create the master instance - master = DirSrv(verbose=False) - master.log.debug("Master allocated") - args = {SER_HOST: HOST_MASTER, - SER_PORT: PORT_MASTER, - SER_SERVERID_PROP: SERVERID_MASTER} - master.allocate(args) - if master.exists(): - master.delete() - master.create() - master.open() - - # Create the consumer instance - consumer = DirSrv(verbose=False) - consumer.log.debug("Consumer allocated") - args = {SER_HOST: HOST_CONSUMER, - SER_PORT: PORT_CONSUMER, - SER_SERVERID_PROP: SERVERID_CONSUMER} - consumer.allocate(args) - if consumer.exists(): - consumer.delete() - consumer.create() - consumer.open() - - # Delete each instance in the end - def fin(): - master.delete() - consumer.delete() - request.addfinalizer(fin) + :feature: Replication + :steps: 1. Enable replication with agreements + 2. Delete the agreements + 3. Check that agreements were deleted + 4. Disable replication + :expectedresults: No errors happen, agreements successfully deleted + """ + + master1 = topo.ins["standalone1"] + master2 = topo.ins["standalone2"] + + log.info("Check that agreements in place") + ents = master1.agreement.list(suffix=NEW_SUFFIX) + assert(len(ents) == 1) + ents = master2.agreement.list(suffix=NEW_SUFFIX) + assert(len(ents) == 1) + + log.info("Delete the agreements") + simple_replica[0].deleteAgreements() + simple_replica[1].deleteAgreements() + + log.info("Check that agreements were deleted") + ents = master1.agreement.list(suffix=NEW_SUFFIX) + assert(len(ents) == 0) + ents = master2.agreement.list(suffix=NEW_SUFFIX) + assert(len(ents) == 0) - return TopologyReplication(master, consumer) +def test_get_ruv_entry(topo, simple_replica): + """Check get_ruv_entry method -def test_create(topology): - """This test creates - - suffix/backend (NEW_SUFFIX_[12], NEW_BACKEND_[12]) : Master - - suffix/backend (NEW_SUFFIX_[12], NEW_BACKEND_[12]) : Consumer - - replica NEW_SUFFIX_1 as MASTER : Master - - replica NEW_SUFFIX_2 as CONSUMER : Master + :feature: Replication + :steps: 1. Enable replication with agreements + 2. Get ruv entry with get_ruv_entry() method + 3. Get ruv entry with ldap.search + 4. Disable replication + :expectedresults: Entries should be equal """ - log.info("\n\n##########\n### CREATE\n############") - # - # MASTER (suffix/backend) - # - backendEntry = topology.master.backend.create( - suffix=NEW_SUFFIX_1, properties={BACKEND_NAME: NEW_BACKEND_1}) - backendEntry = topology.master.backend.create( - suffix=NEW_SUFFIX_2, properties={BACKEND_NAME: NEW_BACKEND_2}) - - ents = topology.master.mappingtree.list() - master_nb_mappingtree = len(ents) - - # create a first additional mapping tree - topology.master.mappingtree.create(NEW_SUFFIX_1, bename=NEW_BACKEND_1) - ents = topology.master.mappingtree.list() - assert len(ents) == (master_nb_mappingtree + 1) - topology.master.add_s(Entry((NEW_SUFFIX_1, - {'objectclass': "top organizationalunit".split(), - 'ou': NEW_SUFFIX_1.split('=', 1)[1]}))) - - # create a second additional mapping tree - topology.master.mappingtree.create(NEW_SUFFIX_2, bename=NEW_BACKEND_2) - ents = topology.master.mappingtree.list() - assert len(ents) == (master_nb_mappingtree + 2) - topology.master.add_s(Entry((NEW_SUFFIX_2, - {'objectclass': "top organizationalunit".split(), - 'ou': NEW_SUFFIX_2.split('=', 1)[1]}))) - log.info('Master it exists now %d suffix(es)' % len(ents)) - - # - # CONSUMER (suffix/backend) - # - backendEntry = topology.consumer.backend.create( - suffix=NEW_SUFFIX_1, properties={BACKEND_NAME: NEW_BACKEND_1}) - backendEntry = topology.consumer.backend.create( - suffix=NEW_SUFFIX_2, properties={BACKEND_NAME: NEW_BACKEND_2}) - - ents = topology.consumer.mappingtree.list() - consumer_nb_mappingtree = len(ents) - - # create a first additional mapping tree - topology.consumer.mappingtree.create(NEW_SUFFIX_1, bename=NEW_BACKEND_1) - ents = topology.consumer.mappingtree.list() - assert len(ents) == (consumer_nb_mappingtree + 1) - topology.consumer.add_s(Entry((NEW_SUFFIX_1, - {'objectclass': "top organizationalunit".split(), - 'ou': NEW_SUFFIX_1.split('=', 1)[1]}))) - - # create a second additional mapping tree - topology.consumer.mappingtree.create(NEW_SUFFIX_2, bename=NEW_BACKEND_2) - ents = topology.consumer.mappingtree.list() - assert len(ents) == (consumer_nb_mappingtree + 2) - topology.consumer.add_s(Entry((NEW_SUFFIX_2, - {'objectclass': "top organizationalunit".split(), - 'ou': NEW_SUFFIX_2.split('=', 1)[1]}))) - log.info('Consumer it exists now %d suffix(es)' % len(ents)) - - # - # Now create REPLICAS on master - # - # check it exists this entry to stores the changelogs - topology.master.changelog.create() - - # create a master - topology.master.replica.create(suffix=NEW_SUFFIX_1, - role=REPLICAROLE_MASTER, - rid=1) - ents = topology.master.replica.list() - assert len(ents) == 1 - log.info('Master replica %s' % ents[0].dn) - - # create a consumer - topology.master.replica.create(suffix=NEW_SUFFIX_2, - role=REPLICAROLE_CONSUMER) - ents = topology.master.replica.list() - assert len(ents) == 2 - ents = topology.master.replica.list(suffix=NEW_SUFFIX_2) - log.info('Consumer replica %s' % ents[0].dn) - - # - # Now create REPLICAS on consumer - # - # create a master - topology.consumer.replica.create(suffix=NEW_SUFFIX_1, - role=REPLICAROLE_CONSUMER) - ents = topology.consumer.replica.list() - assert len(ents) == 1 - log.info('Consumer replica %s' % ents[0].dn) + ruv_entry = simple_replica[0].get_ruv_entry() + entry = topo.ins["standalone1"].search_s(NEW_SUFFIX, ldap.SCOPE_SUBTREE, REPLICA_RUV_FILTER)[0] - # create a consumer - topology.consumer.replica.create(suffix=NEW_SUFFIX_2, - role=REPLICAROLE_CONSUMER) - ents = topology.consumer.replica.list() - assert len(ents) == 2 - ents = topology.consumer.replica.list(suffix=NEW_SUFFIX_2) - log.info('Consumer replica %s' % ents[0].dn) + assert ruv_entry == entry -def test_list(topology): - """This test checks: - - existing replicas can be retrieved - - access to unknown replica does not fail +def test_get_role(topo, simple_replica): + """Check get_role method - PRE-CONDITION: - It exists on MASTER two replicas NEW_SUFFIX_1 and NEW_SUFFIX_2 - created by test_create() + :feature: Replication + :steps: 1. Enable replication with agreements + 2. Get role with get_role() method + 3. Get repl_flags, repl_type from the replica entry with ldap.search + 4. Compare the values + 5. Disable replication + :expectedresults: The role 'master' should have flags=1 and type=3 """ - log.info("\n\n############\n### LIST\n############") - ents = topology.master.replica.list() - assert len(ents) == 2 + replica_role = simple_replica[0].get_role() + replica_flags = simple_replica[0].get_attr_val_int(REPL_FLAGS) + replica_type = simple_replica[0].get_attr_val_int(REPL_TYPE) - # Check we can retrieve a replica with its suffix - ents = topology.master.replica.list(suffix=NEW_SUFFIX_1) - assert len(ents) == 1 - replica_dn_1 = ents[0].dn + log.info("Check that we've got role 'master', while {}=1 and {}=3".format(REPL_FLAGS, REPL_TYPE)) + assert replica_role == ReplicaRole.MASTER and replica_flags == REPLICA_FLAGS_WRITE \ + and replica_type == REPLICA_RDWR_TYPE, \ + "Failure, get_role() gave {}, while {} has {} and {} has {}".format(replica_role, + REPL_FLAGS, replica_flags, + REPL_TYPE, replica_type) - # Check we can retrieve a replica with its suffix - ents = topology.master.replica.list(suffix=NEW_SUFFIX_2) - assert len(ents) == 1 - replica_dn_2 = ents[0].dn - # Check we can retrieve a replica with its DN - ents = topology.master.replica.list(replica_dn=replica_dn_1) - assert len(ents) == 1 - assert replica_dn_1 == ents[0].dn +def test_basic(topo, new_suffixes, clean_up): + """Check basic replica functionality + + :feature: Replication + :steps: 1. Enable replication on master. hub and consumer + 2. Create agreements: master-hub, hub-consumer + 3. Test master-consumer replication + 4. Disable replication + 5. Check that replica, agreements and changelog were deleted + :expectedresults: No errors happen, replication is successfully enabled and disabled + """ - # Check we can retrieve a replica if we provide DN and suffix - ents = topology.master.replica.list(suffix=NEW_SUFFIX_2, - replica_dn=replica_dn_2) + master = topo.ins["standalone1"] + hub = topo.ins["standalone2"] + consumer = topo.ins["standalone3"] + + log.info("Enable replicas (create replica and changelog entries)") + master_replicas = Replicas(master) + master_replicas.enable(suffix=NEW_SUFFIX, + role=ReplicaRole.MASTER, + replicaID=REPLICA_MASTER_ID) + ents = master_replicas.list() + assert len(ents) == 1 + ents = master.changelog.list() assert len(ents) == 1 - assert replica_dn_2 == ents[0].dn - # Check DN is used before suffix name - ents = topology.master.replica.list(suffix=NEW_SUFFIX_2, - replica_dn=replica_dn_1) + hub_replicas = Replicas(hub) + hub_replicas.enable(suffix=NEW_SUFFIX, + role=ReplicaRole.HUB, + replicaID=CONSUMER_REPLICAID) + ents = hub_replicas.list() + assert len(ents) == 1 + ents = hub.changelog.list() assert len(ents) == 1 - assert replica_dn_1 == ents[0].dn - - # Check that invalid value does not break - ents = topology.master.replica.list(suffix="X") - for ent in ents: - log.critical("Unexpected replica: %s" % ent.dn) - assert len(ents) == 0 - - -def test_create_repl_manager(topology): - """The tests are - - create the default Replication manager/Password - - create a specific Replication manager/ default Password - - Check we can bind successfully - - create a specific Replication manager / specific Password - - Check we can bind successfully - """ - log.info("\n\n###########\n### CREATE_REPL_MANAGER\n###########") - # First create the default replication manager - topology.consumer.replica.create_repl_manager() - ents = topology.consumer.search_s(defaultProperties[REPLICATION_BIND_DN], - ldap.SCOPE_BASE, "objectclass=*") + consumer_replicas = Replicas(consumer) + consumer_replicas.enable(suffix=NEW_SUFFIX, + role=ReplicaRole.CONSUMER) + ents = consumer_replicas.list() assert len(ents) == 1 - assert ents[0].dn == defaultProperties[REPLICATION_BIND_DN] - # Second create a custom replication manager under NEW_SUFFIX_2 - rm_dn = "cn=replrepl,%s" % NEW_SUFFIX_2 - topology.consumer.replica.create_repl_manager(repl_manager_dn=rm_dn) - ents = topology.consumer.search_s(rm_dn, ldap.SCOPE_BASE, "objectclass=*") + log.info("Create agreements between the instances") + master.agreement.create(suffix=NEW_SUFFIX, + host=hub.host, + port=hub.port) + ents = master.agreement.list(suffix=NEW_SUFFIX) assert len(ents) == 1 - assert ents[0].dn == rm_dn - - # Check we can bind - topology.consumer.simple_bind_s(rm_dn, - defaultProperties[REPLICATION_BIND_PW]) - - # Check we fail to bind - with pytest.raises(ldap.INVALID_CREDENTIALS) as excinfo: - topology.consumer.simple_bind_s(rm_dn, "dummy") - log.info("Exception: %s" % str(excinfo.value)) - - # now rebind - topology.consumer.simple_bind_s(topology.consumer.binddn, - topology.consumer.bindpw) - - # Create a custom replication manager under NEW_SUFFIX_1 - # with a specified password - rm_dn = NEW_RM_1 - topology.consumer.replica.create_repl_manager(repl_manager_dn=rm_dn, - repl_manager_pw="Secret123") - ents = topology.consumer.search_s(rm_dn, ldap.SCOPE_BASE, "objectclass=*") + hub.agreement.create(suffix=NEW_SUFFIX, + host=consumer.host, + port=consumer.port) + ents = hub.agreement.list(suffix=NEW_SUFFIX) assert len(ents) == 1 - assert ents[0].dn == rm_dn - # Check we can bind - topology.consumer.simple_bind_s(rm_dn, "Secret123") + log.info("Test replication") + master_replicas.test(NEW_SUFFIX, consumer) + + log.info("Disable replication") + master_replicas.disable(suffix=NEW_SUFFIX) + hub_replicas.disable(suffix=NEW_SUFFIX) + consumer_replicas.disable(suffix=NEW_SUFFIX) + + log.info("Check that replica, agreements and changelog were deleted") + for num in range(1, 4): + log.info("Checking standalone{} instance".format(num)) + inst = topo.ins["standalone{}".format(num)] + + log.info("Checking that replica entries don't exist") + replicas = Replicas(inst) + ents = replicas.list() + assert len(ents) == 0 + + log.info("Checking that changelog doesn't exist") + ents = inst.changelog.list() + assert len(ents) == 0 + + log.info("Checking that agreements can't be acquired because the replica entry doesn't exist") + with pytest.raises(NoSuchEntryError) as e: + inst.agreement.list(suffix=NEW_SUFFIX) + assert "no replica set up" in e.msg + + +@pytest.mark.parametrize('role_from,role_to', + ((ReplicaRole.CONSUMER, ReplicaRole.HUB), + (ReplicaRole.CONSUMER, ReplicaRole.MASTER), + (ReplicaRole.HUB, ReplicaRole.MASTER))) +def test_promote(topo, new_suffixes, clean_up, role_from, role_to): + """Check that replica promote method works properly + + :feature: Replication + :steps: 1. Enable replication on the instance + 2. Promote it to another role + (check consumer-hub, consumer-master, hub-master + 3. Check that role was successfully changed + 4. Disable replication + :expectedresults: No errors happen, replica successfully promoted + """ - # Check we fail to bind - with pytest.raises(ldap.INVALID_CREDENTIALS) as excinfo: - topology.consumer.simple_bind_s(rm_dn, "dummy") - log.info("Exception: %s" % str(excinfo.value)) - topology.consumer.simple_bind_s(topology.consumer.binddn, - topology.consumer.bindpw) + inst = topo.ins["standalone1"] + log.info("Enable replication on instance with a role - {}".format(role_from)) + replicas = Replicas(inst) + replica = replicas.enable(suffix=NEW_SUFFIX, + role=role_from) -def test_enableReplication(topology): - """It checks - - Ability to enable replication on a supplier - - Ability to enable replication on a consumer - - Failure to enable replication with wrong replicaID on supplier - - Failure to enable replication with wrong replicaID on consumer - """ + log.info("Promote replica to {}".format(role_to)) + replica.promote(newrole=role_to, + rid=REPLICA_MASTER_ID) - log.info("\n\n############\n### ENABLEREPLICATION\n##########") - # - # MASTER (suffix/backend) - # - backendEntry = topology.master.backend.create(suffix=NEW_SUFFIX_3, - properties={BACKEND_NAME: - NEW_BACKEND_3}) - - ents = topology.master.mappingtree.list() - master_nb_mappingtree = len(ents) - - # create a first additional mapping tree - topology.master.mappingtree.create(NEW_SUFFIX_3, bename=NEW_BACKEND_3) - ents = topology.master.mappingtree.list() - assert len(ents) == (master_nb_mappingtree + 1) - topology.master.add_s(Entry((NEW_SUFFIX_3, - {'objectclass': "top organizationalunit".split(), - 'ou': NEW_SUFFIX_3.split('=', 1)[1]}))) - - # a supplier should have replicaId in [1..CONSUMER_REPLICAID[ - with pytest.raises(ValueError) as excinfo: - topology.master.replica.enableReplication(suffix=NEW_SUFFIX_3, - role=REPLICAROLE_MASTER, - replicaId=CONSUMER_REPLICAID) - log.info("Exception (expected): %s" % str(excinfo.value)) - topology.master.replica.enableReplication(suffix=NEW_SUFFIX_3, - role=REPLICAROLE_MASTER, - replicaId=1) - - # - # MASTER (suffix/backend) - # - backendEntry = topology.master.backend.create(suffix=NEW_SUFFIX_4, - properties={BACKEND_NAME: - NEW_BACKEND_4}) - - ents = topology.master.mappingtree.list() - master_nb_mappingtree = len(ents) - - # create a first additional mapping tree - topology.master.mappingtree.create(NEW_SUFFIX_4, bename=NEW_BACKEND_4) - ents = topology.master.mappingtree.list() - assert len(ents) == (master_nb_mappingtree + 1) - topology.master.add_s(Entry((NEW_SUFFIX_4, - {'objectclass': "top organizationalunit".split(), - 'ou': NEW_SUFFIX_4.split('=', 1)[1]}))) - - # A consumer should have CONSUMER_REPLICAID not '1' - with pytest.raises(ValueError) as excinfo: - topology.master.replica.enableReplication(suffix=NEW_SUFFIX_4, - role=REPLICAROLE_CONSUMER, - replicaId=1) - log.info("Exception (expected): %s" % str(excinfo.value)) - topology.master.replica.enableReplication(suffix=NEW_SUFFIX_4, - role=REPLICAROLE_CONSUMER) - - -def test_disableReplication(topology): - """It checks - - Ability to disable replication on a supplier - - Ability to disable replication on a consumer - - Failure to disable replication with wrong suffix on supplier - - Failure to disable replication with wrong suffix on consumer + log.info("Check that replica was successfully promoted") + replica_role = replica.get_role() + assert replica_role == role_to + + +@pytest.mark.parametrize('role_from,role_to', + ((ReplicaRole.MASTER, ReplicaRole.HUB), + (ReplicaRole.MASTER, ReplicaRole.CONSUMER), + (ReplicaRole.HUB, ReplicaRole.CONSUMER))) +def test_demote(topo, new_suffixes, clean_up, role_from, role_to): + """Check that replica demote method works properly + + :feature: Replication + :steps: 1. Enable replication on the instance + 2. Demote it to another role + (check master-hub, master-consumer, hub-consumer) + 3. Check that role was successfully changed + 4. Disable replication + :expectedresults: No errors happen, replica successfully demoted """ - log.info("\n\n############\n### DISABLEREPLICATION\n##########") - topology.master.replica.disableReplication(suffix=NEW_SUFFIX_3) - with pytest.raises(ldap.LDAPError) as excinfo: - topology.master.replica.disableReplication(suffix=NEW_SUFFIX_3) - log.info("Exception (expected): %s" % str(excinfo.value)) + inst = topo.ins["standalone1"] + + log.info("Enable replication on instance with a role - {}".format(role_from.name)) + replicas = Replicas(inst) + replica = replicas.enable(suffix=NEW_SUFFIX, + role=role_from, + replicaID=REPLICA_MASTER_ID) + + log.info("Promote replica to {}".format(role_to.name)) + replica.demote(newrole=role_to) - topology.master.replica.disableReplication(suffix=NEW_SUFFIX_4) - with pytest.raises(ldap.LDAPError) as excinfo: - topology.master.replica.disableReplication(suffix=NEW_SUFFIX_4) - log.info("Exception (expected): %s" % str(excinfo.value)) + log.info("Check that replica was successfully promoted") + replica_role = replica.get_role() + assert replica_role == role_to -def test_setProperties(topology): - """Set some properties - Verified that valid properties are set - Verified that invalid properties raise an Exception +@pytest.mark.parametrize('role_from', (ReplicaRole.MASTER, + ReplicaRole.HUB, + ReplicaRole.CONSUMER)) +def test_promote_fail(topo, new_suffixes, clean_up, role_from): + """Check that replica promote method fails + when promoted to wrong direction - PRE-REQUISITE: it exists a replica for NEW_SUFFIX_1 + :feature: Replication + :steps: 1. Enable replication on the instance + 2. Try to promote it to wrong role + (for example, master-hub, hub-consumer) + 3. Disable replication + :expectedresults: Replica shouldn't be promoted """ - log.info("\n\n##########\n### SETPROPERTIES\n############") - # set valid values to SUFFIX_1 - properties = {REPLICA_LEGACY_CONS: 'off', - REPLICA_BINDDN: NEW_RM_1, - REPLICA_PURGE_INTERVAL: str(3600), - REPLICA_PURGE_DELAY: str(5 * 24 * 3600), - REPLICA_REFERRAL: "ldap://%s:1234/" % LOCALHOST} - topology.master.replica.setProperties(suffix=NEW_SUFFIX_1, - properties=properties) - - # Check the values have been written - replicas = topology.master.replica.list(suffix=NEW_SUFFIX_1) - assert len(replicas) == 1 - for prop in properties: - attr = REPLICA_PROPNAME_TO_ATTRNAME[prop] - val = replicas[0].getValue(attr) - log.info("Replica[%s] -> %s: %s" % (prop, attr, val)) - assert val == properties[prop] - - # Check invalid properties raise exception - with pytest.raises(ValueError) as excinfo: - properties = {"dummy": 'dummy'} - topology.master.replica.setProperties(suffix=NEW_SUFFIX_1, - properties=properties) - log.info("Exception (expected): %s" % str(excinfo.value)) - - # check call without suffix/dn/entry raise InvalidArgumentError - with pytest.raises(InvalidArgumentError) as excinfo: - properties = {REPLICA_LEGACY_CONS: 'off'} - topology.master.replica.setProperties(properties=properties) - log.info("Exception (expected): %s" % str(excinfo.value)) - - # check that if we do not provide a valid entry it raises ValueError - with pytest.raises(ValueError) as excinfo: - properties = {REPLICA_LEGACY_CONS: 'off'} - topology.master.replica.setProperties(replica_entry="dummy", - properties=properties) - log.info("Exception (expected): %s" % str(excinfo.value)) - - # check that with an invalid suffix or replica_dn it raise ValueError - with pytest.raises(ValueError) as excinfo: - properties = {REPLICA_LEGACY_CONS: 'off'} - topology.master.replica.setProperties(suffix="dummy", - properties=properties) - log.info("Exception (expected): %s" % str(excinfo.value)) - - -def test_getProperties(topology): - """Currently not implemented""" - - log.info("\n\n############\n### GETPROPERTIES\n###########") - with pytest.raises(NotImplementedError) as excinfo: - properties = {REPLICA_LEGACY_CONS: 'off'} - topology.master.replica.getProperties(suffix=NEW_SUFFIX_1) - log.info("Exception (expected): %s" % str(excinfo.value)) + inst = topo.ins["standalone1"] + + log.info("Enable replication on instance with a role - {}".format(role_from.name)) + replicas = Replicas(inst) + replica = replicas.enable(suffix=NEW_SUFFIX, + role=role_from, + replicaID=REPLICA_MASTER_ID) + + for role_to in [x for x in range(1, 4) if x <= role_from.value]: + role_to = ReplicaRole(role_to) + log.info("Try to promote replica to {}".format(role_to.name)) + with pytest.raises(ValueError): + replica.promote(newrole=role_to, + rid=REPLICA_MASTER_ID) + + +@pytest.mark.parametrize('role_from', (ReplicaRole.MASTER, + ReplicaRole.HUB, + ReplicaRole.CONSUMER)) +def test_demote_fail(topo, new_suffixes, clean_up, role_from): + """Check that replica demote method fails + when demoted to wrong direction + + :feature: Replication + :steps: 1. Enable replication on the instance + 2. Try to demote it to wrong role + (for example, consumer-master, hub-master) + 3. Disable replication + :expectedresults: Replica shouldn't be demoted + """ + + inst = topo.ins["standalone1"] + + log.info("Enable replication on instance with a role - {}".format(role_from.name)) + replicas = Replicas(inst) + replica = replicas.enable(suffix=NEW_SUFFIX, + role=role_from, + replicaID=REPLICA_MASTER_ID) + + for role_to in [x for x in range(1, 4) if x >= role_from.value]: + role_to = ReplicaRole(role_to) + log.info("Try to demote replica to {}".format(role_to.name)) + with pytest.raises(ValueError): + replica.demote(newrole=role_to) if __name__ == "__main__": diff --git a/lib389/topologies.py b/lib389/topologies.py index 161d887..225e138 100644 --- a/lib389/topologies.py +++ b/lib389/topologies.py @@ -16,8 +16,7 @@ from lib389 import DirSrv from lib389.utils import generate_ds_params from lib389.replica import Replicas from lib389._constants import (args_instance, SER_HOST, SER_PORT, SER_SERVERID_PROP, SER_CREATION_SUFFIX, - ROLE_STANDALONE, REPLICAROLE_MASTER, REPLICAROLE_HUB, REPLICAROLE_CONSUMER, - DEFAULT_SUFFIX, REPLICA_ID) + ReplicaRole, DEFAULT_SUFFIX, REPLICA_ID) DEBUGGING = os.getenv('DEBUGGING', default=False) if DEBUGGING: @@ -35,7 +34,7 @@ def create_topology(topo_dict): """ if not topo_dict: - ValueError("You need to specify the dict. For instance: {ROLE_STANDALONE: 1}") + ValueError("You need to specify the dict. For instance: {ReplicaRole.STANDALONE: 1}") instances = {} ms = {} @@ -68,22 +67,22 @@ def create_topology(topo_dict): instance.delete() instance.create() instance.open() - if role == ROLE_STANDALONE: + if role == ReplicaRole.STANDALONE: ins[instance.serverid] = instance instances.update(ins) - if role == REPLICAROLE_MASTER: + if role == ReplicaRole.MASTER: ms[instance.serverid] = instance instances.update(ms) - if role == REPLICAROLE_HUB: + if role == ReplicaRole.HUB: hs[instance.serverid] = instance instances.update(hs) - if role == REPLICAROLE_CONSUMER: + if role == ReplicaRole.CONSUMER: cs[instance.serverid] = instance instances.update(cs) log.info("Instance with parameters {} was created.".format(args_copied)) # Set up replication - if role in (REPLICAROLE_MASTER, REPLICAROLE_HUB, REPLICAROLE_CONSUMER): + if role in (ReplicaRole.MASTER, ReplicaRole.HUB, ReplicaRole.CONSUMER): replicas = Replicas(instance) replica = replicas.enable(DEFAULT_SUFFIX, role, instance_data[REPLICA_ID]) replica_dict[replica] = instance @@ -91,16 +90,18 @@ def create_topology(topo_dict): # Create agreements for role_from in topo_dict.keys(): for inst_num_from in range(1, topo_dict[role]+1): - roles_to = [REPLICAROLE_HUB, REPLICAROLE_CONSUMER] - if role == REPLICAROLE_MASTER: - roles_to.append(REPLICAROLE_MASTER) + roles_to = [ReplicaRole.HUB, ReplicaRole.CONSUMER] + if role == ReplicaRole.MASTER: + roles_to.append(ReplicaRole.MASTER) for role_to in [role for role in topo_dict if role in roles_to]: for inst_num_to in range(1, topo_dict[role]+1): # Exclude our instance if role_from != role_to or inst_num_from != inst_num_to: - inst_from = instances["{}{}".format(role_from, inst_num_from)] - inst_to = instances["{}{}".format(role_to, inst_num_to)] + inst_from_id = "{}{}".format(role_from.name.lower(), inst_num_from) + inst_to_id = "{}{}".format(role_to.name.lower(), inst_num_to) + inst_from = instances[inst_from_id] + inst_to = instances[inst_to_id] agmt = inst_from.agreement.create(suffix=DEFAULT_SUFFIX, host=inst_to.host, port=inst_to.port) @@ -163,7 +164,7 @@ class TopologyMain(object): def topology_st(request): """Create DS standalone instance""" - topology = create_topology({ROLE_STANDALONE: 1}) + topology = create_topology({ReplicaRole.STANDALONE: 1}) def fin(): if DEBUGGING: @@ -179,7 +180,7 @@ def topology_st(request): def topology_i2(request): """Create two instance DS deployment""" - topology = create_topology({ROLE_STANDALONE: 2}) + topology = create_topology({ReplicaRole.STANDALONE: 2}) def fin(): if DEBUGGING: @@ -195,7 +196,7 @@ def topology_i2(request): def topology_i3(request): """Create three instance DS deployment""" - topology = create_topology({ROLE_STANDALONE: 3}) + topology = create_topology({ReplicaRole.STANDALONE: 3}) def fin(): if DEBUGGING: @@ -211,8 +212,8 @@ def topology_i3(request): def topology_m1c1(request): """Create Replication Deployment with one master and one consumer""" - topology = create_topology({REPLICAROLE_MASTER: 1, - REPLICAROLE_CONSUMER: 1}) + topology = create_topology({ReplicaRole.MASTER: 1, + ReplicaRole.CONSUMER: 1}) replicas = Replicas(topology.ms["master1"]) replicas.test(DEFAULT_SUFFIX, topology.cs["consumer1"]) @@ -230,9 +231,9 @@ def topology_m1c1(request): def topology_m1h1c1(request): """Create Replication Deployment with one master, one consumer and one hub""" - topology = create_topology({REPLICAROLE_MASTER: 1, - REPLICAROLE_HUB: 1, - REPLICAROLE_CONSUMER: 1}) + topology = create_topology({ReplicaRole.MASTER: 1, + ReplicaRole.HUB: 1, + ReplicaRole.CONSUMER: 1}) replicas = Replicas(topology.ms["master1"]) replicas.test(DEFAULT_SUFFIX, topology.cs["consumer1"]) @@ -250,7 +251,7 @@ def topology_m1h1c1(request): def topology_m2(request): """Create Replication Deployment with two masters""" - topology = create_topology({REPLICAROLE_MASTER: 2}) + topology = create_topology({ReplicaRole.MASTER: 2}) replicas = Replicas(topology.ms["master1"]) replicas.test(DEFAULT_SUFFIX, topology.ms["master2"]) @@ -268,7 +269,7 @@ def topology_m2(request): def topology_m3(request): """Create Replication Deployment with three masters""" - topology = create_topology({REPLICAROLE_MASTER: 3}) + topology = create_topology({ReplicaRole.MASTER: 3}) replicas = Replicas(topology.ms["master1"]) replicas.test(DEFAULT_SUFFIX, topology.ms["master3"]) @@ -286,7 +287,7 @@ def topology_m3(request): def topology_m4(request): """Create Replication Deployment with four masters""" - topology = create_topology({REPLICAROLE_MASTER: 4}) + topology = create_topology({ReplicaRole.MASTER: 4}) replicas = Replicas(topology.ms["master1"]) replicas.test(DEFAULT_SUFFIX, topology.ms["master4"]) @@ -304,8 +305,8 @@ def topology_m4(request): def topology_m2c2(request): """Create Replication Deployment with two masters and two consumers""" - topology = create_topology({REPLICAROLE_MASTER: 2, - REPLICAROLE_CONSUMER: 2}) + topology = create_topology({ReplicaRole.MASTER: 2, + ReplicaRole.CONSUMER: 2}) replicas = Replicas(topology.ms["master1"]) replicas.test(DEFAULT_SUFFIX, topology.cs["consumer1"]) diff --git a/lib389/utils.py b/lib389/utils.py index 5cb3230..70748a6 100644 --- a/lib389/utils.py +++ b/lib389/utils.py @@ -39,8 +39,8 @@ from contextlib import closing import lib389 from lib389.paths import Paths from lib389._constants import ( - DEFAULT_USER, VALGRIND_WRAPPER, DN_CONFIG, CFGSUFFIX, LOCALHOST, ROLE_STANDALONE, - REPLICAROLE_MASTER, REPLICAROLE_HUB, REPLICAROLE_CONSUMER, CONSUMER_REPLICAID + DEFAULT_USER, VALGRIND_WRAPPER, DN_CONFIG, CFGSUFFIX, LOCALHOST, + ReplicaRole, CONSUMER_REPLICAID ) from lib389.properties import ( SER_HOST, SER_USER_ID, SER_GROUP_ID, SER_STRICT_HOSTNAME_CHECKING, SER_PORT, @@ -747,44 +747,45 @@ def formatInfData(args): return content -def generate_ds_params(inst_num, role=ROLE_STANDALONE): +def generate_ds_params(inst_num, role=ReplicaRole.STANDALONE): """Generate a host, port, secure port, server ID and replica ID for the selected role and instance number. I.e. inst_num=1, role="master". @param inst_num - an instance number in a range from 1 to 99 - @param role - ROLE_STANDALONE, REPLICAROLE_MASTER, REPLICAROLE_HUB, REPLICAROLE_CONSUMER + @param role - ReplicaRole.STANDALONE, ReplicaRole.MASTER, ReplicaRole.HUB, ReplicaRole.CONSUMER @return - the dict with next keys: host, port, secure port, server id and replica id """ if inst_num not in range(1, 100): raise ValueError("Instance number should be in a range from 1 to 99") - if role not in (ROLE_STANDALONE, REPLICAROLE_MASTER, REPLICAROLE_HUB, REPLICAROLE_CONSUMER): - raise ValueError('Role should be {}, {}, {}, {}'.format(ROLE_STANDALONE, REPLICAROLE_MASTER, - REPLICAROLE_HUB, REPLICAROLE_CONSUMER)) + if role not in (ReplicaRole.STANDALONE, ReplicaRole.MASTER, ReplicaRole.HUB, ReplicaRole.CONSUMER): + raise ValueError('Role should be {}, {}, {}, {}'.format(ReplicaRole.STANDALONE, ReplicaRole.MASTER, + ReplicaRole.HUB, ReplicaRole.CONSUMER)) instance_data = {} relevant_num = 38900 # Set relevant number for ports - if role == REPLICAROLE_MASTER: + if role == ReplicaRole.MASTER: relevant_num += 100 - elif role == REPLICAROLE_HUB: + elif role == ReplicaRole.HUB: relevant_num += 200 - elif role == REPLICAROLE_CONSUMER: + elif role == ReplicaRole.CONSUMER: relevant_num += 300 # Define replica ID - if role == REPLICAROLE_MASTER: + if role == ReplicaRole.MASTER: replica_id = inst_num else: replica_id = CONSUMER_REPLICAID # Fill the dict with data + server_id = "{}{}".format(role.name.lower(), inst_num) instance_data[SER_HOST] = LOCALHOST instance_data[SER_PORT] = relevant_num + inst_num instance_data[SER_SECURE_PORT] = relevant_num + inst_num + 24700 - instance_data[SER_SERVERID_PROP] = "{}{}".format(role, inst_num) + instance_data[SER_SERVERID_PROP] = server_id instance_data[REPLICA_ID] = replica_id return instance_data diff --git a/python-lib389.spec b/python-lib389.spec index 3200e59..88901c9 100644 --- a/python-lib389.spec +++ b/python-lib389.spec @@ -34,6 +34,7 @@ Requires: krb5-workstation Requires: krb5-server Requires: openssl Requires: iproute +Requires: python-enum34 # Conditional will need to change later. %if 0%{?rhel} >= 8 || 0%{?fedora} Requires: python2 diff --git a/requirements.txt b/requirements.txt index 1f58b78..f208938 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ pyasn1 pyasn1-modules six pytest -python-dateutil \ No newline at end of file +python-dateutil +enum34 -- 2.13.3