From 5b05a3c4a00bb6fb923db08f10b952393b37d492 Mon Sep 17 00:00:00 2001 From: Mark Reynolds Date: Mon, 16 Feb 2015 16:23:23 -0500 Subject: [PATCH] Ticket 48029 - Add missing replication related functions Description: There was either missing or not fully implemented functions for disabling replication, deleting agreements, and deleting the changelog. In agreement.py variable 'format' was overiding the built-in 'format', so it was renamed to raformat. Also added some helper functions to "grep" log files, or any file. https://fedorahosted.org/389/ticket/48029 Reviewed by: ? --- lib389/__init__.py | 20 +++- lib389/_constants.py | 9 +- lib389/agreement.py | 50 ++++++++-- lib389/changelog.py | 34 ++++--- lib389/properties.py | 41 ++++---- lib389/replica.py | 260 +++++++++++++++++++++++++++++---------------------- lib389/tasks.py | 28 ++++-- lib389/tools.py | 20 +++- 8 files changed, 292 insertions(+), 170 deletions(-) diff --git a/lib389/__init__.py b/lib389/__init__.py index 645e5dc..e109909 100644 --- a/lib389/__init__.py +++ b/lib389/__init__.py @@ -1770,14 +1770,16 @@ class DirSrv(SimpleLDAPObject): @raise None ''' - test_value = 'test replication - ' + str(int(time.time())) + test_value = ('test replication from ' + self.serverid + ' to ' + + replicas[0].serverid + ': ' + str(int(time.time()))) self.modify_s(suffix, [(ldap.MOD_REPLACE, 'description', test_value)]) for replica in replicas: loop = 0 replicated = False - while loop <= 10: + while loop <= 30: try: + entry = replica.getEntry(suffix, ldap.SCOPE_BASE, "(objectclass=*)") if entry.hasValue('description', test_value): replicated = True @@ -1787,7 +1789,7 @@ class DirSrv(SimpleLDAPObject): % (suffix, e.message['desc'])) return False loop += 1 - time.sleep(1) + time.sleep(2) if not replicated: log.fatal('Replication is not in sync with replica server (%s)' % replica.serverid) return False @@ -2239,3 +2241,15 @@ class DirSrv(SimpleLDAPObject): self.start(timeout=10) return result + + def searchAccessLog(self, pattern): + return DirSrvTools.searchFile(self.accesslog, pattern) + + def searchAuditLog(self, pattern): + return DirSrvTools.searchFile(self.auditlog, pattern) + + def searchErrorsLog(self, pattern): + return DirSrvTools.searchFile(self.errlog, pattern) + + def detectDisorderlyShutdown(self): + return DirSrvTools.searchFile(self.errlog, DISORDERLY_SHUTDOWN) \ No newline at end of file diff --git a/lib389/_constants.py b/lib389/_constants.py index 5ba7fe7..2b4c6d5 100644 --- a/lib389/_constants.py +++ b/lib389/_constants.py @@ -19,8 +19,9 @@ 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_RUV_UUID = "ffffffff-ffffffff-ffffffff-ffffffff" -REPLICA_OC_TOMBSTONE= "nsTombstone" +REPLICA_RUV_UUID = "ffffffff-ffffffff-ffffffff-ffffffff" +REPLICA_RUV_FILTER = '(&(nsuniqueid=ffffffff-ffffffff-ffffffff-ffffffff)(objectclass=nstombstone))' +REPLICA_OC_TOMBSTONE = "nsTombstone" REPLICATION_BIND_DN = 'replication_bind_dn' REPLICATION_BIND_PW = 'replication_bind_pw' REPLICATION_BIND_METHOD = 'replication_bind_method' @@ -119,6 +120,7 @@ RETROCL_SUFFIX = "cn=changelog" ### ################################## +PLUGIN_7_BIT_CHECK = '7-bit check' PLUGIN_ACCT_POLICY = 'Account Policy Plugin' PLUGIN_ACCT_USABILITY = 'Account Usability Plugin' PLUGIN_ACL = 'ACL Plugin' @@ -152,13 +154,14 @@ PLUGIN_WHOAMI = 'whoami' # -# constants +# Constants # DEFAULT_USER = "nobody" DEFAULT_USERHOME = "/tmp/lib389_home" DATA_DIR = "data" TMP_DIR = "tmp" VALGRIND_WRAPPER = "ns-slapd.valgrind" +DISORDERLY_SHUTDOWN = 'Detected Disorderly Shutdown last time Directory Server was running, recovering database' # # LOG: see https://access.redhat.com/site/documentation/en-US/Red_Hat_Directory_Server/9.0/html/Administration_Guide/Configuring_Logs.html diff --git a/lib389/agreement.py b/lib389/agreement.py index 95cfea8..e94bbb2 100644 --- a/lib389/agreement.py +++ b/lib389/agreement.py @@ -108,7 +108,8 @@ class Agreement(object): if ((hour < 0) or (hour > 23)): raise ValueError("Bad schedule format %r: illegal hour %d" % (interval, hour)) if int(schedule[1]) > int(schedule[3]): - raise ValueError("Bad schedule (start HOUR larger than end HOUR) %r: illegal hour %d" % (interval, int(schedule[1]))) + raise ValueError("Bad schedule (start HOUR larger than end HOUR) %r: illegal hour %d" % + (interval, int(schedule[1]))) # check the minutes minute = int(schedule[2]) @@ -362,11 +363,12 @@ class Agreement(object): # Now returns the replica agreement for that suffix that replicates to # consumer host/port if consumer_host and consumer_port: - filt = "(&(objectclass=%s)(%s=%s)(%s=%d))" % (RA_OBJECTCLASS_VALUE, + 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)" % RA_OBJECTCLASS_VALUE + filt = "(|(objectclass=%s)(objectclass=%s))" % (RA_OBJECTCLASS_VALUE, RA_WINDOWS_OBJECTCLASS_VALUE) return self.conn.search_s(replica_entry.dn, ldap.SCOPE_ONELEVEL, filt) def create(self, suffix=None, host=None, port=None, properties=None): @@ -520,6 +522,39 @@ class Agreement(object): return dn_agreement + def delete(self, suffix, replica): + ''' + Delete a replication agreement + + @param suffix - the suffix that the agreement is configured for + @replica - a DirSrv object of the server that the agreement points to + @raise ldap.LDAPError - for ldap operation failures + ''' + + if replica.sslport: + # We assume that if you are using SSL, you are using it with replication + port = replica.sslport + else: + port = replica.port + + agmts = self.list(suffix=suffix, consumer_host=replica.host, consumer_port=port) + if agmts: + if len(agmts) > 1: + # Found too many agreements + self.log.error('Too many agreements found') + raise + else: + # delete the agreement + try: + self.conn.delete_s(agmts[0].dn) + except ldap.LDAPError, e: + self.log.error('Failed to delete agreement (%s), error: %s' % + (agmts[0].dn, e.message['desc'])) + raise ldap.LDAPError + else: + # No agreements, error? + self.log.error('No agreements found') + def init(self, suffix=None, consumer_host=None, consumer_port=None): """Trigger a total update of the consumer replica - self is the supplier, @@ -547,7 +582,7 @@ class Agreement(object): 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) @@ -557,10 +592,13 @@ class Agreement(object): 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) + 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)) + self.log.fatal("initAgreement: No replica agreement to %s:%d for suffix %s" % + (consumer_host, consumer_port, nsuffix)) raise # diff --git a/lib389/changelog.py b/lib389/changelog.py index 53c5592..025e71e 100644 --- a/lib389/changelog.py +++ b/lib389/changelog.py @@ -26,14 +26,14 @@ class Changelog(object): def list(self, suffix=None, changelogdn=None): if not changelogdn: raise InvalidArgumentError("changelog DN is missing") - + base = changelogdn filtr = "(objectclass=extensibleobject)" - + # now do the effective search ents = self.conn.search_s(base, ldap.SCOPE_BASE, filtr) return ents - + def create(self, dbname=DEFAULT_CHANGELOG_DB): """Add and return the replication changelog entry. @@ -56,24 +56,32 @@ class Changelog(object): except ldap.ALREADY_EXISTS: self.log.warn("entry %s already exists" % dn) return(dn) - - def delete(self, changelogdn=None): - raise NotImplemented - + + def delete(self): + ''' + Delete the changelog entry + @raise LDAPError + ''' + try: + self.conn.delete_s(DN_CHANGELOG) + except ldap.LDAPError, e: + self.log.error('Failed to delete the changelog: ' + e.message['desc']) + raise ldap.LDAPError + 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: @@ -85,12 +93,12 @@ class Changelog(object): op = ldap.MOD_DELETE else: op = ldap.MOD_REPLACE - + mods.append((op, REPLICA_PROPNAME_TO_ATTRNAME[val], properties[prop])) - + # that is fine now to apply the MOD self.conn.modify_s(ents[0].dn, mods) - + def getProperties(self, changelogdn=None, properties=None): raise NotImplemented \ No newline at end of file diff --git a/lib389/properties.py b/lib389/properties.py index ef3599a..bc0da7e 100644 --- a/lib389/properties.py +++ b/lib389/properties.py @@ -5,9 +5,9 @@ Created on Dec 5, 2013 ''' #################################### -# +# # Properties supported by the server -# +# #################################### # @@ -36,11 +36,11 @@ SER_BACKUP_INST_DIR ='inst-backupdir' SER_CREATION_SUFFIX ='suffix' #################################### -# +# # Properties supported by the Mapping Tree entries # #################################### - + # Properties name MT_STATE = 'state' MT_BACKEND = 'backend-name' @@ -77,7 +77,7 @@ MT_PROPNAME_TO_ATTRNAME = {MT_STATE: 'nsslapd-state', MT_CHAIN_UPDATE: 'nsslapd-distribution-root-update'} #################################### -# +# # Properties supported by the backend # #################################### @@ -111,7 +111,7 @@ BACKEND_PROPNAME_TO_ATTRNAME = {BACKEND_SUFFIX: 'nsslapd-suffix', BACKEND_CHAIN_URLS: 'nsfarmserverurl'} #################################### -# +# # Properties of a replica # #################################### @@ -142,7 +142,7 @@ REPLICA_PROPNAME_TO_ATTRNAME = { REPLICA_FLAGS: 'nsds5flags'} #################################### -# +# # Properties of a changelog # #################################### @@ -161,7 +161,7 @@ CHANGELOG_PROPNAME_TO_ATTRNAME = {CHANGELOG_NAME: 'cn', CHANGELOG_COMPACT_INTV: 'nsslapd-changelogcompactdb-interval',} #################################### -# +# # Properties of an entry # #################################### @@ -178,7 +178,7 @@ ENTRY_TYPE_TO_OBJECTCLASS = {ENTRY_TYPE_PERSON: ["top", "person"], ENTRY_TYPE_INETPERSON: ["top", "person", "inetOrgPerson"]} #################################### -# +# # Properties supported by the replica agreement # #################################### @@ -201,6 +201,7 @@ RA_TIMEOUT = 'timeout' RA_CHANGES = 'changes' RA_OBJECTCLASS_VALUE = "nsds5replicationagreement" +RA_WINDOWS_OBJECTCLASS_VALUE = "nsDSWindowsReplicationAgreement" RA_PROPNAME_TO_ATTRNAME = {RA_NAME: 'cn', RA_SUFFIX: 'nsds5replicaroot', @@ -220,7 +221,7 @@ RA_PROPNAME_TO_ATTRNAME = {RA_NAME: 'cn', RA_CHANGES: 'nsds5replicaChangesSentSinceStartup'} #################################### -# +# # Properties supported by the plugins # #################################### @@ -232,13 +233,13 @@ PLUGIN_ENABLE = 'enable' PLUGINS_OBJECTCLASS_VALUE = "nsSlapdPlugin" PLUGINS_ENABLE_ON_VALUE = "on" PLUGINS_ENABLE_OFF_VALUE = "off" - + PLUGIN_PROPNAME_TO_ATTRNAME = {PLUGIN_NAME: 'cn', PLUGIN_PATH: 'nsslapd-pluginPath', PLUGIN_ENABLE: 'nsslapd-pluginEnabled'} #################################### -# +# # Properties supported by the index # #################################### @@ -251,7 +252,7 @@ INDEX_PROPNAME_TO_ATTRNAME = {INDEX_TYPE: 'nsIndexType', INDEX_MATCHING_RULE: 'nsMatchingRule'} #################################### -# +# # Properties supported by the tasks # #################################### @@ -265,27 +266,27 @@ def rawProperty(prop): ''' Return the string 'prop' without the heading '+'/'-' @param prop - string of a property name - + @return property name without heading '+'/'-' - + @raise None ''' if str(prop).startswith('+') or str(prop).startswith('-'): return prop[1::] else: return prop - - + + def inProperties(prop, properties): ''' Return True if 'prop' is in the 'properties' dictionary Properties in 'properties' does NOT contain a heading '+'/'-', but 'prop' may contain heading '+'/'-' @param prop - string of a property name - @param properties - dictionary of properties. - + @param properties - dictionary of properties. + @return True/False - + @raise None ''' if rawProperty(prop) in properties: diff --git a/lib389/replica.py b/lib389/replica.py index 5357207..f49cdc7 100644 --- a/lib389/replica.py +++ b/lib389/replica.py @@ -15,9 +15,6 @@ from lib389.properties import * from lib389.utils import normalizeDN, escapeDNValue - - - class Replica(object): proxied_methods = 'search_s getEntry'.split() @@ -34,24 +31,24 @@ class Replica(object): """Return the replica dn of the given suffix.""" mtent = self.conn.mappingtree.list(suffix=suffix) return ','.join(("cn=replica", mtent.dn)) - + @staticmethod def _valid_role(role): if role != REPLICAROLE_MASTER and role != REPLICAROLE_HUB and role != REPLICAROLE_CONSUMER: return False else: return True - + @staticmethod def _valid_rid(role, rid=None): if role == REPLICAROLE_MASTER: - if not decimal.Decimal(rid) or (rid <= 0) or (rid >=CONSUMER_REPLICAID): + if not decimal.Decimal(rid) or (rid <= 0) or (rid >= CONSUMER_REPLICAID): return False else: if rid and (rid != CONSUMER_REPLICAID): return False return True - + @staticmethod def _set_or_default(attribute, properties, default): ''' @@ -64,19 +61,19 @@ class Replica(object): if attribute in properties or add_attribute in properties or del_attribute in properties: return properties[attribute] = default - + def create_repl_manager(self, repl_manager_dn=None, repl_manager_pw=None): ''' Create an entry that will be used to bind as replica manager. - + @param repl_manager_dn - DN of the bind entry. If not provided use the default one @param repl_manager_pw - Password of the entry. If not provide use the default one - + @return None - + @raise - KeyError if can not find valid values of Bind DN and Pwd ''' - + # check the DN and PW try: repl_manager_dn = repl_manager_dn or defaultProperties[REPLICATION_BIND_DN] @@ -89,7 +86,7 @@ class Replica(object): if not repl_manager_dn: self.log.warning("replica_createReplMgr: bind DN not specified") raise - + # if the replication manager entry already exists, ust return try: entries = self.search_s(repl_manager_dn, ldap.SCOPE_BASE, "objectclass=*") @@ -98,9 +95,9 @@ class Replica(object): return except ldap.NO_SUCH_OBJECT: pass - + # ok it does not exist, create it - try: + try: attrs = { 'nsIdleTimeout': '0', 'passwordExpirationTime': '20381010000000Z' @@ -108,76 +105,74 @@ class Replica(object): self.conn.setupBindDN(repl_manager_dn, repl_manager_pw, attrs) except ldap.ALREADY_EXISTS: self.log.warn("User already exists (weird we just checked: %s " % repl_manager_dn) - - + def list(self, suffix=None, replica_dn=None): """ - Return a list of replica entries. - If 'replica_dn' is specified it returns the related entry. + Return a list of replica entries. + If 'replica_dn' is specified it returns the related entry. If 'suffix' is specified it returns the replica that is configured for that 'suffix'. If both 'replica_dn' and 'suffix' are specified it returns the 'replica_dn' entry. If none of them are specified, it returns all the replicas - + @param suffix - suffix of a replica @param replica_dn - DN of a replica - + @return replica entries - + @raise search exceptions: ldap.NO_SUCH_OBJECT, .. """ # set base/filtr according to the arguments if replica_dn: - base = replica_dn + base = replica_dn filtr = "(objectclass=%s)" % REPLICA_OBJECTCLASS_VALUE elif suffix: - base = DN_MAPPING_TREE - filtr = "(&(objectclass=%s)(%s=%s))" % (REPLICA_OBJECTCLASS_VALUE, + base = DN_MAPPING_TREE + filtr = "(&(objectclass=%s)(%s=%s))" % (REPLICA_OBJECTCLASS_VALUE, REPLICA_PROPNAME_TO_ATTRNAME[REPLICA_SUFFIX], suffix) else: - base = DN_MAPPING_TREE + base = DN_MAPPING_TREE filtr = "(objectclass=%s)" % REPLICA_OBJECTCLASS_VALUE - + # now do the effective search ents = self.conn.search_s(base, ldap.SCOPE_SUBTREE, filtr) return ents - def setProperties(self, suffix=None, replica_dn=None, replica_entry=None, properties=None): ''' Set the properties of the replica. If an 'replica_entry' (Entry) is provided, it updates the entry, else it updates the entry on the server. If the 'replica_dn' is provided it retrieves the entry using it, else it retrieve the replica using the 'suffix'. - + @param suffix : suffix stored in that replica (online update) @param replica_dn: DN of the replica (online update) @param replica_entry: Entry of a replica (offline update) @param properties: dictionary of properties Supported properties are: REPLICA_SUFFIX - REPLICA_ID - REPLICA_TYPE - REPLICA_LEGACY_CONS - REPLICA_BINDDN - REPLICA_PURGE_INTERVAL + REPLICA_ID + REPLICA_TYPE + REPLICA_LEGACY_CONS + REPLICA_BINDDN + REPLICA_PURGE_INTERVAL REPLICA_PURGE_DELAY REPLICA_PRECISE_PURGING - REPLICA_REFERRAL - REPLICA_FLAGS - + 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 + 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 @@ -185,16 +180,16 @@ class Replica(object): 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) @@ -203,8 +198,8 @@ class Replica(object): replica_entry.update({REPLICA_PROPNAME_TO_ATTRNAME[val]: properties[prop]}) return - - # If it provides the suffix or the replicaDN, replica.list will + + # 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: @@ -213,7 +208,6 @@ class Replica(object): else: raise ValueError("invalid suffix: %s" % suffix) - # build the MODS mods = [] for prop in properties: @@ -225,12 +219,11 @@ class Replica(object): op = ldap.MOD_DELETE else: op = ldap.MOD_REPLACE - + mods.append((op, REPLICA_PROPNAME_TO_ATTRNAME[val], properties[prop])) - + # that is fine now to apply the MOD self.conn.modify_s(ents[0].dn, mods) - def getProperties(self, suffix=None, replica_dn=None, replica_entry=None, properties=None): raise NotImplementedError @@ -238,28 +231,28 @@ class Replica(object): 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. + @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_SUFFIX + REPLICA_ID + REPLICA_TYPE REPLICA_LEGACY_CONS ['off'] REPLICA_BINDDN [defaultProperties[REPLICATION_BIND_DN]] - REPLICA_PURGE_INTERVAL + 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 @@ -272,31 +265,31 @@ class Replica(object): 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 @@ -305,15 +298,15 @@ class Replica(object): 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]]) + 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 + + # create replica entry in mapping-tree mtents = self.conn.mappingtree.list(suffix=nsuffix) mtent = mtents[0] dn_replica = ','.join((RDN_REPLICA, mtent.dn)) @@ -325,7 +318,7 @@ class Replica(object): return dn_replica except ldap.NO_SUCH_OBJECT: entry = None - + # # Now create the replica entry # @@ -339,85 +332,132 @@ class Replica(object): self.conn.suffixes[nsuffix] = {'dn': dn_replica, 'type': rtype} return dn_replica - - def delete(self, suffix=None): + + def deleteAgreements(self, suffix=None): + ''' + Delete all the agreements for the suffix ''' - 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. + # 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 + + 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 + @param suffix - dn of suffix @return None - - @raise ldap.NO_SUCH_OBJECT - if the suffix has no replica - InvalidArgumentError - if suffix is missing + @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("Replica.delete: suffix is missing") + self.log.fatal("disableReplication: suffix is missing") raise InvalidArgumentError("suffix missing") else: nsuffix = normalizeDN(suffix) - - # check the replica exists + + # 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: - entry = self.conn.getEntry(dn_replica, ldap.SCOPE_BASE) - except ldap.NO_SUCH_OBJECT: - raise - - raise NotImplementedError() - + self.deleteAgreements(nsuffix) + except ldap.LDAPError, e: + self.log.fatal('Failed to delete replica agreements!') + raise ldap.LDAPError + + # Delete the replica + try: + self.conn.delete_s(dn_replica) + except ldap.LDAPError, e: + self.log.fatal('Failed to delete replica configuration (%s), error: %s' % (dn_replica, e.message('desc'))) + raise ldap.LDAPError + + # Delete the changelog + try: + self.conn.changelog.delete() + except ldap.LDAPError, e: + self.log.error('Failed to delete changelog: ' + e.message['desc']) + raise ldap.LDAPError + def enableReplication(self, suffix=None, role=None, replicaId=CONSUMER_REPLICAID, binddn=None): if not suffix: self.log.fatal("enableReplication: suffix not specified") raise ValueError("suffix missing") - + if not role: self.log.fatal("enableReplication: replica role not specify (REPLICAROLE_*)") raise ValueError("role missing") - - # + + # # Check the validity of the parameters # - + # First role and replicaID if role != REPLICAROLE_MASTER and role != REPLICAROLE_HUB and role != REPLICAROLE_CONSUMER: self.log.fatal("enableReplication: replica role invalid (%s) " % role) raise ValueError("invalid role: %s" % role) - + # master replica_type = REPLICA_RDWR_TYPE else: # hub or consumer replica_type = REPLICA_RDONLY_TYPE - + if role == REPLICAROLE_MASTER: # check the replicaId [1..CONSUMER_REPLICAID[ - if not decimal.Decimal(replicaId) or (replicaId <= 0) or (replicaId >=CONSUMER_REPLICAID): + if not decimal.Decimal(replicaId) or (replicaId <= 0) or (replicaId >= CONSUMER_REPLICAID): self.log.fatal("enableReplication: invalid replicaId (%s) for a RW replica" % replicaId) raise ValueError("invalid replicaId %d (expected [1..CONSUMER_REPLICAID[" % replicaId) elif replicaId != CONSUMER_REPLICAID: # check the replicaId is CONSUMER_REPLICAID - self.log.fatal("enableReplication: invalid replicaId (%s) for a Read replica (expected %d)" % (replicaId, CONSUMER_REPLICAID)) + self.log.fatal("enableReplication: invalid replicaId (%s) for a Read replica (expected %d)" % + (replicaId, CONSUMER_REPLICAID)) raise ValueError("invalid replicaId: %d for HUB/CONSUMER replicaId is CONSUMER_REPLICAID" % replicaId) - + # Now check we have a suffix entries_backend = self.conn.backend.list(suffix=suffix) if not entries_backend: self.log.fatal("enableReplication: enable to retrieve the backend for %s" % suffix) raise ValueError("no backend for suffix %s" % suffix) - + ent = entries_backend[0] if normalizeDN(suffix) != normalizeDN(ent.getValue('nsslapd-suffix')): - self.log.warning("enableReplication: suffix (%s) and backend suffix (%s) differs" % (suffix, entries_backend[0].nsslapd-suffix)) + self.log.warning("enableReplication: suffix (%s) and backend suffix (%s) differs" % + (suffix, entries_backend[0].nsslapd - suffix)) pass - + # Now prepare the bindDN property if not binddn: binddn = defaultProperties.get(REPLICATION_BIND_DN, None) @@ -427,29 +467,22 @@ class Replica(object): # property will be set self.log.warning("enableReplication: binddn not provided and default value unavailable") pass - + # Now do the effectif job # First add the changelog if master/hub if (role == REPLICAROLE_MASTER) or (role == REPLICAROLE_HUB): self.conn.changelog.create() - + # Second create the default replica manager entry if it does not exist # it should not be called from here but for the moment I am unsure when to create it elsewhere self.conn.replica.create_repl_manager() - + # then enable replication properties = {REPLICA_BINDDN: [binddn]} ret = self.conn.replica.create(suffix=suffix, role=role, rid=replicaId, args=properties) - + return ret - - def disableReplication(self, suffix=None): - if not suffix: - self.log.fatal("enableReplication: suffix not specified") - raise ValueError("suffix missing") - - raise NotImplementedError - + def check_init(self, agmtdn): """returns tuple - first element is done/not done, 2nd is no error/has error @param agmtdn - the agreement dn @@ -517,10 +550,9 @@ class Replica(object): mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')] self.conn.modify_s(agmtdn, mod) - def keep_in_sync(self, agmtdn): """ - @param agmtdn - + @param agmtdn - """ self.log.info("Setting agreement for continuous replication") raise NotImplementedError("Check nsds5replicaupdateschedule before writing!") @@ -551,5 +583,5 @@ class Replica(object): raise NoSuchEntryError("RUV not found: suffix: %r" % suffix) - + diff --git a/lib389/tasks.py b/lib389/tasks.py index a1ed368..0ec61ba 100644 --- a/lib389/tasks.py +++ b/lib389/tasks.py @@ -776,25 +776,28 @@ class Tasks(object): self.log.info("Sysconfig Reload task (%s) completed successfully" % (cn)) return exitCode - - def cleanAllRUV(self, replicaid=None, force=None, args=None): + def cleanAllRUV(self, suffix=None, replicaid=None, force=None, args=None): ''' @param replicaid - The replica ID to remove/clean @param force - True/False - Clean all the replicas, even if one is down @param args - is a dictionary that contains modifier of the task wait: True/[False] - If True, waits for the completion of the task before to return - @return exit code + @return tuple (task dn, and the exit code) @raise ValueError: If missing replicaid ''' if not replicaid: raise ValueError("Missing required paramter: replicaid") + if not suffix: + raise ValueError("Missing required paramter: suffix") + cn = 'task-' + time.strftime("%m%d%Y_%H%M%S", time.localtime()) dn = ('cn=%s,cn=cleanallruv,cn=tasks,cn=config' % cn) entry = Entry(dn) entry.setValues('objectclass', 'top', 'extensibleObject') entry.setValues('cn', cn) + entry.setValues('replica-base-dn', suffix) entry.setValues('replica-id', replicaid) if force: entry.setValues('replica-force-cleaning', 'yes') @@ -803,7 +806,7 @@ class Tasks(object): self.conn.add_s(entry) except ldap.ALREADY_EXISTS: self.log.error("Fail to add cleanAllRUV task") - return -1 + return (dn, -1) exitCode = 0 if args and args.get(TASK_WAIT, False): @@ -813,36 +816,41 @@ class Tasks(object): self.log.error("Error: cleanAllRUV task (%s) exited with %d" % (cn, exitCode)) else: self.log.info("cleanAllRUV task (%s) completed successfully" % (cn)) - return exitCode + return (dn, exitCode) - def abortCleanAllRUV(self, replicaid=None, certify=None, args=None): + def abortCleanAllRUV(self, suffix=None, replicaid=None, certify=None, args=None): ''' @param replicaid - The replica ID to remove/clean @param certify - True/False - Certify the task was aborted on all the replicas @param args - is a dictionary that contains modifier of the task wait: True/[False] - If True, waits for the completion of the task before to return - @return exit code + @return tuple (task dn, and the exit code) @raise ValueError: If missing replicaid ''' if not replicaid: raise ValueError("Missing required paramter: replicaid") + if not suffix: + raise ValueError("Missing required paramter: suffix") cn = 'task-' + time.strftime("%m%d%Y_%H%M%S", time.localtime()) dn = ('cn=%s,cn=abort cleanallruv,cn=tasks,cn=config' % cn) entry = Entry(dn) entry.setValues('objectclass', 'top', 'extensibleObject') entry.setValues('cn', cn) + entry.setValues('replica-base-dn', suffix) entry.setValues('replica-id', replicaid) if certify: - entry.setValues('replica-certifyall', 'yes') + entry.setValues('replica-certify-all', 'yes') + else: + entry.setValues('replica-certify-all', 'no') # start the task and possibly wait for task completion try: self.conn.add_s(entry) except ldap.ALREADY_EXISTS: self.log.error("Fail to add Abort cleanAllRUV task") - return -1 + return (dn, -1) exitCode = 0 if args and args.get(TASK_WAIT, False): @@ -852,7 +860,7 @@ class Tasks(object): self.log.error("Error: Abort cleanAllRUV task (%s) exited with %d" % (cn, exitCode)) else: self.log.info("Abort cleanAllRUV task (%s) completed successfully" % (cn)) - return exitCode + return (dn, exitCode) def upgradeDB(self, nsArchiveDir=None, nsDatabaseType=None, nsForceToReindex=None, args=None): ''' diff --git a/lib389/tools.py b/lib389/tools.py index c73dec4..738e406 100644 --- a/lib389/tools.py +++ b/lib389/tools.py @@ -882,7 +882,8 @@ class DirSrvTools(object): assert(words[1] == expectedHost) done = True except AssertionError: - raise AssertionError("Error: /etc/hosts should contains 'localhost.localdomain' as first host for %s" % (expectedHost, loopbackIpPattern)) + raise AssertionError("Error: /etc/hosts should contains 'localhost.localdomain' as first host for %s" % + (expectedHost, loopbackIpPattern)) @staticmethod def runUpgrade(prefix, online=True): @@ -951,6 +952,21 @@ class DirSrvTools(object): log.fatal('runUpgrade failed!') assert False + @staticmethod + def searchFile(filename, pattern): + # Open the file and read it line by line + found = False + try: + myfile = open(filename) + for line in myfile: + if re.search(pattern, line): + found = True + myfile.close() + except IOError as e: + log.error('Problem opening/searching file (%s): I/O error(%d): %s' % (filename, e.errno, e.strerror)) + + return found + class MockDirSrv(object): host = 'localhost' @@ -969,3 +985,5 @@ class MockDirSrv(object): return 'ldaps://%s:%s' % (self.host, self.sslport) else: return 'ldap://%s:%s' % (self.host, self.port) + + -- 1.9.3