From 4881826e1b7996862f5549c7caad28e44f8fda0f Mon Sep 17 00:00:00 2001 From: Mark Reynolds Date: Sep 14 2018 14:18:52 +0000 Subject: Ticket 49926 - Add replication functionality to dsconf Description: Add replication functionality to the dsconf. This includes repl config, agmts, winsync agmts, and cleanallruv/abort cleanallruv Adjusted the backend options to use hyphens for consistency https://pagure.io/389-ds-base/issue/49926 Reviewed by: spichugi & firstyear(Thanks!!) --- diff --git a/ldap/servers/plugins/replication/repl5_agmt.c b/ldap/servers/plugins/replication/repl5_agmt.c index 20a0ca9..6e60dd6 100644 --- a/ldap/servers/plugins/replication/repl5_agmt.c +++ b/ldap/servers/plugins/replication/repl5_agmt.c @@ -3029,8 +3029,9 @@ agmt_update_maxcsn(Replica *r, Slapi_DN *sdn, int op, LDAPMod **mods, CSN *csn) * temporarily mark it as "unavailable". */ slapi_ch_free_string(&agmt->maxcsn); - agmt->maxcsn = slapi_ch_smprintf("%s;%s;%s;%" PRId64 ";unavailable", slapi_sdn_get_dn(agmt->replarea), - slapi_rdn_get_value_by_ref(slapi_rdn_get_rdn(agmt->rdn)), agmt->hostname, agmt->port); + agmt->maxcsn = slapi_ch_smprintf("%s;%s;%s;%" PRId64 ";unavailable;%s", slapi_sdn_get_dn(agmt->replarea), + slapi_rdn_get_value_by_ref(slapi_rdn_get_rdn(agmt->rdn)), agmt->hostname, + agmt->port, maxcsn); } else if (rid == oprid) { slapi_ch_free_string(&agmt->maxcsn); agmt->maxcsn = slapi_ch_smprintf("%s;%s;%s;%" PRId64 ";%" PRIu16 ";%s", slapi_sdn_get_dn(agmt->replarea), diff --git a/src/lib389/cli/dsconf b/src/lib389/cli/dsconf index ac8af23..9b64589 100755 --- a/src/lib389/cli/dsconf +++ b/src/lib389/cli/dsconf @@ -26,6 +26,7 @@ from lib389.cli_conf import health as cli_health from lib389.cli_conf import saslmappings as cli_sasl from lib389.cli_conf import pwpolicy as cli_pwpolicy from lib389.cli_conf import backup as cli_backup +from lib389.cli_conf import replication as cli_replication from lib389.cli_conf.plugins import memberof as cli_memberof from lib389.cli_conf.plugins import usn as cli_usn from lib389.cli_conf.plugins import rootdn_ac as cli_rootdn_ac @@ -79,6 +80,7 @@ cli_automember.create_parser(subparsers) cli_sasl.create_parser(subparsers) cli_pwpolicy.create_parser(subparsers) cli_backup.create_parser(subparsers) +cli_replication.create_parser(subparsers) argcomplete.autocomplete(parser) diff --git a/src/lib389/lib389/__init__.py b/src/lib389/lib389/__init__.py index 47d6a2e..0bb4378 100644 --- a/src/lib389/lib389/__init__.py +++ b/src/lib389/lib389/__init__.py @@ -80,6 +80,7 @@ from lib389.utils import ( formatInfData, ensure_bytes, ensure_str, + ensure_list_str, format_cmd_list) from lib389.paths import Paths from lib389.nss_ssl import NssSsl @@ -3286,21 +3287,28 @@ class DirSrv(SimpleLDAPObject, object): ldif_file, e.errno, e.strerror) raise e - def getConsumerMaxCSN(self, replica_entry): + def getConsumerMaxCSN(self, replica_entry, binddn=None, bindpw=None): """ Attempt to get the consumer's maxcsn from its database """ - host = replica_entry.getValue(AGMT_HOST) - port = replica_entry.getValue(AGMT_PORT) - suffix = replica_entry.getValue(REPL_ROOT) + host = replica_entry.get_attr_val_utf8(AGMT_HOST) + port = replica_entry.get_attr_val_utf8(AGMT_PORT) + suffix = replica_entry.get_attr_val_utf8(REPL_ROOT) error_msg = "Unavailable" + # If we are using LDAPI we need to provide the credentials, otherwise + # use the existing credentials + if binddn is None: + binddn = self.binddn + if bindpw is None: + bindpw = self.bindpw + # Open a connection to the consumer consumer = DirSrv(verbose=self.verbose) args_instance[SER_HOST] = host args_instance[SER_PORT] = int(port) - args_instance[SER_ROOT_DN] = self.binddn - args_instance[SER_ROOT_PW] = self.bindpw + args_instance[SER_ROOT_DN] = binddn + args_instance[SER_ROOT_PW] = bindpw args_standalone = args_instance.copy() consumer.allocate(args_standalone) try: @@ -3317,7 +3325,7 @@ class DirSrv(SimpleLDAPObject, object): # Error consumer.close() return None - rid = replica_entries[0].getValue(REPL_ID) + rid = ensure_str(replica_entries[0].getValue(REPL_ID)) except: # Error consumer.close() @@ -3330,8 +3338,9 @@ class DirSrv(SimpleLDAPObject, object): consumer.close() if not entry: # Error out? + self.log.error("Failed to retrieve database RUV entry from consumer") return error_msg - elements = entry[0].getValues('nsds50ruv') + elements = ensure_list_str(entry[0].getValues('nsds50ruv')) for ruv in elements: if ('replica %s ' % rid) in ruv: ruv_parts = ruv.split() @@ -3345,16 +3354,17 @@ class DirSrv(SimpleLDAPObject, object): consumer.close() return error_msg - def getReplAgmtStatus(self, agmt_entry): + def getReplAgmtStatus(self, agmt_entry, binddn=None, bindpw=None): ''' Return the status message, if consumer is not in synch raise an exception ''' agmt_maxcsn = None - suffix = agmt_entry.getValue(REPL_ROOT) - agmt_name = agmt_entry.getValue('cn') + suffix = agmt_entry.get_attr_val_utf8(REPL_ROOT) + agmt_name = agmt_entry.get_attr_val_utf8('cn') status = "Unknown" rc = -1 + try: entry = self.search_s(suffix, ldap.SCOPE_SUBTREE, REPLICA_RUV_FILTER, [AGMT_MAXCSN]) @@ -3373,7 +3383,8 @@ class DirSrv(SimpleLDAPObject, object): dc=example,dc=com;test_agmt;localhost;389;unavailable ''' - maxcsns = entry[0].getValues(AGMT_MAXCSN) + + maxcsns = ensure_list_str(entry[0].getValues(AGMT_MAXCSN)) for csn in maxcsns: comps = csn.split(';') if agmt_name == comps[1]: @@ -3384,19 +3395,19 @@ class DirSrv(SimpleLDAPObject, object): agmt_maxcsn = comps[5] if agmt_maxcsn: - con_maxcsn = self.getConsumerMaxCSN(agmt_entry) + con_maxcsn = self.getConsumerMaxCSN(agmt_entry, binddn=binddn, bindpw=bindpw) if con_maxcsn: if agmt_maxcsn == con_maxcsn: status = "In Synchronization" rc = 0 else: - # Not in sync - attmpt to discover the cause + # Not in sync - attempt to discover the cause repl_msg = "Unknown" - if agmt_entry.getValue(AGMT_UPDATE_IN_PROGRESS) == 'TRUE': + if agmt_entry.get_attr_val_utf8(AGMT_UPDATE_IN_PROGRESS) == 'TRUE': # Replication is on going - this is normal repl_msg = "Replication still in progress" elif "Can't Contact LDAP" in \ - agmt_entry.getValue(AGMT_UPDATE_STATUS): + agmt_entry.get_attr_val_utf8(AGMT_UPDATE_STATUS): # Consumer is down repl_msg = "Consumer can not be contacted" diff --git a/src/lib389/lib389/_mapped_object.py b/src/lib389/lib389/_mapped_object.py index c347a5f..5c0e0b6 100644 --- a/src/lib389/lib389/_mapped_object.py +++ b/src/lib389/lib389/_mapped_object.py @@ -169,7 +169,7 @@ class DSLdapObject(DSLogging): str_attrs[ensure_str(k)] = ensure_list_str(attrs[k]) # ensure all the keys are lowercase - str_attrs = dict((k.lower(), v) for k, v in str_attrs.items()) + str_attrs = dict((k.lower(), v) for k, v in list(str_attrs.items())) response = json.dumps({"type": "entry", "dn": ensure_str(self._dn), "attrs": str_attrs}) @@ -969,7 +969,7 @@ class DSLdapObjects(DSLogging): # This may not work in all cases, especially when we consider plugins. # co = self._entry_to_instance(dn=None, entry=None) - # Make the rdn naming attr avaliable + # Make the rdn naming attr available self._rdn_attribute = co._rdn_attribute (rdn, properties) = self._validate(rdn, properties) # Now actually commit the creation req diff --git a/src/lib389/lib389/_replication.py b/src/lib389/lib389/_replication.py index 7d51577..caa0c64 100644 --- a/src/lib389/lib389/_replication.py +++ b/src/lib389/lib389/_replication.py @@ -83,6 +83,14 @@ class CSN(object): retstr = "equal" return retstr + def get_time_lag(self, oth): + diff = oth.ts - self.ts + if diff < 0: + lag = datetime.timedelta(seconds=-diff) + else: + lag = datetime.timedelta(seconds=diff) + return "{:0>8}".format(str(lag)) + def __repr__(self): return ("%s seq: %s rid: %s" % (time.strftime("%x %X", time.localtime(self.ts)), str(self.seq), str(self.rid))) diff --git a/src/lib389/lib389/agreement.py b/src/lib389/lib389/agreement.py index 9e8d90f..8d03ef9 100644 --- a/src/lib389/lib389/agreement.py +++ b/src/lib389/lib389/agreement.py @@ -10,13 +10,13 @@ import ldap import re import time import six - +import json +import datetime from lib389._constants import * from lib389.properties import * from lib389._entry import FormatDict -from lib389.utils import normalizeDN, ensure_bytes, ensure_str, ensure_dict_str +from lib389.utils import normalizeDN, ensure_bytes, ensure_str, ensure_dict_str, ensure_list_str from lib389 import Entry, DirSrv, NoSuchEntryError, InvalidArgumentError - from lib389._mapped_object import DSLdapObject, DSLdapObjects @@ -33,16 +33,25 @@ class Agreement(DSLdapObject): :type dn: str """ - def __init__(self, instance, dn=None): + csnpat = r'(.{8})(.{4})(.{4})(.{4})' + csnre = re.compile(csnpat) + + def __init__(self, instance, dn=None, winsync=False): super(Agreement, self).__init__(instance, dn) self._rdn_attribute = 'cn' self._must_attributes = [ 'cn', ] - self._create_objectclasses = [ - 'top', - 'nsds5replicationagreement', - ] + if winsync: + self._create_objectclasses = [ + 'top', + 'nsDSWindowsReplicationAgreement', + ] + else: + self._create_objectclasses = [ + 'top', + 'nsds5replicationagreement', + ] self._protected = False def begin_reinit(self): @@ -59,6 +68,7 @@ class Agreement(DSLdapObject): """ done = False error = False + inprogress = False status = self.get_attr_val_utf8('nsds5ReplicaLastInitStatus') self._log.debug('agreement tot_init status: %s' % status) if not status: @@ -67,33 +77,300 @@ class Agreement(DSLdapObject): error = True elif 'Total update succeeded' in status: done = True + inprogress = False elif 'Replication error' in status: error = True + elif 'Total update in progress' in status: + inprogress = True + elif 'LDAP error' in status: + error = True - return (done, error) + return (done, inprogress, error) def wait_reinit(self, timeout=300): """Wait for a reinit to complete. Returns done and error. A correct reinit will return (True, False). - + :param timeout: timeout value for how long to wait for the reinit + :type timeout: int :returns: tuple(done, error), where done, error are bool. """ done = False error = False count = 0 while done is False and error is False: - (done, error) = self.check_reinit() + (done, inprogress, error) = self.check_reinit() if count > timeout and not done: error = True count = count + 2 time.sleep(2) return (done, error) + def get_agmt_maxcsn(self): + """Get the agreement maxcsn from the database RUV entry + :returns: CSN string if found, otherwise None is returned + """ + from lib389.replica import Replicas + suffix = self.get_attr_val_utf8(REPL_ROOT) + agmt_name = self.get_attr_val_utf8('cn') + replicas = Replicas(self._instance) + replica = replicas.get(suffix) + maxcsns = replica.get_ruv_agmt_maxcsns() + + if maxcsns is None or len(maxcsns) == 0: + self._log.debug('get_agmt_maxcsn - Failed to get agmt maxcsn from RUV') + return None + + for csn in maxcsns: + comps = csn.split(';') + if agmt_name == comps[1]: + # same replica, get maxcsn + if len(comps) < 6: + return None + else: + return comps[5] + + self._log.debug('get_agmt_maxcsn - did not find matching agmt maxcsn from RUV') + return None + + def get_consumer_maxcsn(self, binddn=None, bindpw=None): + """Attempt to get the consumer's maxcsn from its database RUV entry + :param binddn: Specifies a specific bind DN to use when contacting the remote consumer + :type binddn: str + :param bindpw: Password for the bind DN + :type bindpw: str + :returns: CSN string if found, otherwise "Unavailable" is returned + """ + host = self.get_attr_val_utf8(AGMT_HOST) + port = self.get_attr_val_utf8(AGMT_PORT) + suffix = self.get_attr_val_utf8(REPL_ROOT) + protocol = self.get_attr_val_utf8('nsds5replicatransportinfo').lower() + + result_msg = "Unavailable" + + # If we are using LDAPI we need to provide the credentials, otherwise + # use the existing credentials + if binddn is None: + binddn = self._instance.binddn + if bindpw is None: + bindpw = self._instance.bindpw + + # Get the replica id from supplier to compare to the consumer's rid + from lib389.replica import Replicas + replicas = Replicas(self._instance) + replica = replicas.get(suffix) + rid = replica.get_attr_val_utf8(REPL_ID) + + # Open a connection to the consumer + consumer = DirSrv(verbose=self._instance.verbose) + args_instance[SER_HOST] = host + if protocol == "ssl" or protocol == "ldaps": + args_instance[SER_SECURE_PORT] = int(port) + else: + args_instance[SER_PORT] = int(port) + args_instance[SER_ROOT_DN] = binddn + args_instance[SER_ROOT_PW] = bindpw + args_standalone = args_instance.copy() + consumer.allocate(args_standalone) + try: + consumer.open() + except ldap.LDAPError as e: + self._instance.log.debug('Connection to consumer ({}:{}) failed, error: {}'.format(host, port, e)) + return result_msg + + # Search for the tombstone RUV entry + try: + entry = consumer.search_s(suffix, ldap.SCOPE_SUBTREE, + REPLICA_RUV_FILTER, ['nsds50ruv']) + if not entry: + self.log.error("Failed to retrieve database RUV entry from consumer") + else: + elements = ensure_list_str(entry[0].getValues('nsds50ruv')) + for ruv in elements: + if ('replica %s ' % rid) in ruv: + ruv_parts = ruv.split() + if len(ruv_parts) == 5: + result_msg = ruv_parts[4] + break + except ldap.LDAPError as e: + self._instance.log.debug('Failed to search for the suffix ' + + '({}) consumer ({}:{}) failed, error: {}'.format( + suffix, host, port, e)) + consumer.close() + return result_msg + + def get_agmt_status(self, binddn=None, bindpw=None): + """Return the status message + :param binddn: Specifies a specific bind DN to use when contacting the remote consumer + :type binddn: str + :param bindpw: Password for the bind DN + :type bindpw: str + :returns: A status message about the replication agreement + """ + status = "Unknown" + + agmt_maxcsn = self.get_agmt_maxcsn() + if agmt_maxcsn is not None: + con_maxcsn = self.get_consumer_maxcsn(binddn=binddn, bindpw=bindpw) + if con_maxcsn: + if agmt_maxcsn == con_maxcsn: + status = "In Synchronization" + else: + # Not in sync - attempt to discover the cause + repl_msg = "Unknown" + if self.get_attr_val_utf8(AGMT_UPDATE_IN_PROGRESS) == 'TRUE': + # Replication is on going - this is normal + repl_msg = "Replication still in progress" + elif "Can't Contact LDAP" in \ + self.get_attr_val_utf8(AGMT_UPDATE_STATUS): + # Consumer is down + repl_msg = "Consumer can not be contacted" + + status = ("Not in Synchronization: supplier " + + "(%s) consumer (%s) Reason(%s)" % + (agmt_maxcsn, con_maxcsn, repl_msg)) + return status + + def get_lag_time(self, suffix, agmt_name, binddn=None, bindpw=None): + """Get the lag time between the supplier and the consumer + :param suffix: The replication suffix + :type suffix: str + :param agmt_name: The name of the agreement + :type agmt_name: str + :param binddn: Specifies a specific bind DN to use when contacting the remote consumer + :type binddn: str + :param bindpw: Password for the bind DN + :type bindpw: str + :returns: A time-formated string of the the replication lag (HH:MM:SS). + :raises: ValueError - if unable to get consumer's maxcsn + """ + agmt_maxcsn = self.get_agmt_maxcsn() + con_maxcsn = self.get_consumer_maxcsn(binddn=binddn, bindpw=bindpw) + if con_maxcsn is None: + raise ValueError("Unable to get consumer's max csn") + if con_maxcsn == "Unavailable": + return con_maxcsn + + # Extract the csn timstamps and compare them + match = Agreement.csnre.match(agmt_maxcsn) + if match: + agmt_time = int(match.group(1), 16) + match = Agreement.csnre.match(con_maxcsn) + if match: + con_time = int(match.group(1), 16) + diff = con_time - agmt_time + if diff < 0: + lag = datetime.timedelta(seconds=-diff) + else: + lag = datetime.timedelta(seconds=diff) + + # Return a nice formated timestamp + return "{:0>8}".format(str(lag)) + + def status(self, winsync=False, just_status=False, use_json=False, binddn=None, bindpw=None): + """Get the status of a replication agreement + :param winsync: Specifies if the the agreement is a winsync replication agreement + :type winsync: boolean + :param just_status: Just return the status string and not all of the status attributes + :type just_status: boolean + :param use_json: Return the status in a JSON object + :type use_json: boolean + :param binddn: Specifies a specific bind DN to use when contacting the remote consumer + :type binddn: str + :param bindpw: Password for the bind DN + :type bindpw: str + :returns: A status message + :raises: ValueError - if failing to get agmt status + """ + status_attrs_dict = self.get_all_attrs() + status_attrs_dict = dict((k.lower(), v) for k, v in list(status_attrs_dict.items())) + + # We need a bind DN and passwd so we can query the consumer. If this is an LDAPI + # connection, and the consumer does not allow anonymous access to the tombstone + # RUV entry under the suffix, then we can't get the status. So in this case we + # need to provide a DN and password. + if not winsync: + try: + status = self.get_agmt_status(binddn=binddn, bindpw=bindpw) + except ValueError as e: + status = str(e) + if just_status: + if use_json: + return (json.dumps(status)) + else: + return status + + # Get the lag time + suffix = ensure_str(status_attrs_dict['nsds5replicaroot'][0]) + agmt_name = ensure_str(status_attrs_dict['cn'][0]) + lag_time = self.get_lag_time(suffix, agmt_name, binddn=binddn, bindpw=bindpw) + else: + status = "Not available for Winsync agreements" + + # handle the attributes that are not always set in the agreement + if 'nsds5replicaenabled' not in status_attrs_dict: + status_attrs_dict['nsds5replicaenabled'] = ['on'] + if 'nsds5agmtmaxcsn' not in status_attrs_dict: + status_attrs_dict['nsds5agmtmaxcsn'] = ["unavailable"] + if 'nsds5replicachangesskippedsince' not in status_attrs_dict: + status_attrs_dict['nsds5replicachangesskippedsince'] = ["unavailable"] + if 'nsds5beginreplicarefresh' not in status_attrs_dict: + status_attrs_dict['nsds5beginreplicarefresh'] = [""] + if 'nsds5replicalastinitstatus' not in status_attrs_dict: + status_attrs_dict['nsds5replicalastinitstatus'] = ["unavilable"] + if 'nsds5replicachangessentsincestartup' not in status_attrs_dict: + status_attrs_dict['nsds5replicachangessentsincestartup'] = ['0'] + if ensure_str(status_attrs_dict['nsds5replicachangessentsincestartup'][0]) == '': + status_attrs_dict['nsds5replicachangessentsincestartup'] = ['0'] + + # Case sensitive? + if use_json: + result = {'replica-enabled': ensure_str(status_attrs_dict['nsds5replicaenabled'][0]), + 'update-in-progress': ensure_str(status_attrs_dict['nsds5replicaupdateinprogress'][0]), + 'last-update-start': ensure_str(status_attrs_dict['nsds5replicalastupdatestart'][0]), + 'last-update-end': ensure_str(status_attrs_dict['nsds5replicalastupdateend'][0]), + 'number-changes-sent': ensure_str(status_attrs_dict['nsds5replicachangessentsincestartup'][0]), + 'number-changes-skipped:': ensure_str(status_attrs_dict['nsds5replicachangesskippedsince'][0]), + 'last-update-status': ensure_str(status_attrs_dict['nsds5replicalastupdatestatus'][0]), + 'init-in-progress': ensure_str(status_attrs_dict['nsds5beginreplicarefresh'][0]), + 'last-init-start': ensure_str(status_attrs_dict['nsds5replicalastinitstart'][0]), + 'last-init-end': ensure_str(status_attrs_dict['nsds5replicalastinitend'][0]), + 'last-init-status': ensure_str(status_attrs_dict['nsds5replicalastinitstatus'][0]), + 'reap-active': ensure_str(status_attrs_dict['nsds5replicareapactive'][0]), + 'replication-status': status, + 'replication-lag-time': lag_time + } + return (json.dumps(result)) + else: + retstr = ( + "Status for %(cn)s agmt %(nsDS5ReplicaHost)s:" + "%(nsDS5ReplicaPort)s" "\n" + "Replica Enabled: %(nsds5ReplicaEnabled)s" "\n" + "Update In Progress: %(nsds5replicaUpdateInProgress)s" "\n" + "Last Update Start: %(nsds5replicaLastUpdateStart)s" "\n" + "Last Update End: %(nsds5replicaLastUpdateEnd)s" "\n" + "Number Of Changes Sent: %(nsds5replicaChangesSentSinceStartup)s" + "\n" + "Number Of Changes Skipped: %(nsds5replicaChangesSkippedSince" + "Startup)s" "\n" + "Last Update Status: %(nsds5replicaLastUpdateStatus)s" "\n" + "Init In Progress: %(nsds5BeginReplicaRefresh)s" "\n" + "Last Init Start: %(nsds5ReplicaLastInitStart)s" "\n" + "Last Init End: %(nsds5ReplicaLastInitEnd)s" "\n" + "Last Init Status: %(nsds5ReplicaLastInitStatus)s" "\n" + "Reap Active: %(nsds5ReplicaReapActive)s" "\n" + ) + # FormatDict manages missing fields in string formatting + entry_data = ensure_dict_str(status_attrs_dict) + result = retstr % FormatDict(entry_data) + result += "Replication Status: {}\n".format(status) + result += "Replication Lag Time: {}\n".format(lag_time) + return result + def pause(self): """Pause outgoing changes from this server to consumer. Note that this does not pause the consumer, only that changes will not be sent from this master to consumer: the consumer may still - recieve changes from other replication paths! + receive changes from other replication paths! """ self.set('nsds5ReplicaEnabled', 'off') @@ -122,6 +399,34 @@ class Agreement(DSLdapObject): """ return self.get_attr_val_utf8('nsDS5ReplicaWaitForAsyncResults') + +class WinsyncAgreement(Agreement): + """A replication agreement from this server instance to + another instance of directory server. + + - must attributes: [ 'cn' ] + - RDN attribute: 'cn' + + :param instance: An instance + :type instance: lib389.DirSrv + :param dn: Entry DN + :type dn: str + """ + + def __init__(self, instance, dn=None): + super(Agreement, self).__init__(instance, dn) + self._rdn_attribute = 'cn' + self._must_attributes = [ + 'cn', + ] + self._create_objectclasses = [ + 'top', + 'nsDSWindowsReplicationAgreement', + ] + + self._protected = False + + class Agreements(DSLdapObjects): """Represents the set of agreements configured on this instance. There are two possible ways to use this interface. @@ -149,11 +454,15 @@ class Agreements(DSLdapObjects): :type rdn: str """ - def __init__(self, instance, basedn=DN_MAPPING_TREE, rdn=None): + def __init__(self, instance, basedn=DN_MAPPING_TREE, rdn=None, winsync=False): super(Agreements, self).__init__(instance) - self._childobject = Agreement - self._objectclasses = [ 'nsds5replicationagreement' ] - self._filterattrs = [ 'cn', 'nsDS5ReplicaRoot' ] + if winsync: + self._childobject = WinsyncAgreement + self._objectclasses = ['nsDSWindowsReplicationAgreement'] + else: + self._childobject = Agreement + self._objectclasses = ['nsds5replicationagreement'] + self._filterattrs = ['cn', 'nsDS5ReplicaRoot'] if rdn is None: self._basedn = basedn else: @@ -167,6 +476,7 @@ class Agreements(DSLdapObjects): raise ldap.UNWILLING_TO_PERFORM("Refusing to create agreement in %s" % DN_MAPPING_TREE) return super(Agreements, self)._validate(rdn, properties) + class AgreementLegacy(object): """An object that helps to work with agreement entry @@ -194,7 +504,6 @@ class AgreementLegacy(object): :type agreement_dn: str :param just_status: If True, returns just status :type just_status: bool - :returns: str -- See below :raises: NoSuchEntryError - if agreement_dn is an unknown entry @@ -208,7 +517,7 @@ class AgreementLegacy(object): Last Update End: 0 Num. Changes Sent: 1:10/0 Num. changes Skipped: None - Last update Status: 0 Replica acquired successfully: + Last update Status: Error (0) Replica acquired successfully: Incremental update started Init in progress: None Last Init Start: 0 diff --git a/src/lib389/lib389/changelog.py b/src/lib389/lib389/changelog.py index d973a57..4cb3063 100644 --- a/src/lib389/lib389/changelog.py +++ b/src/lib389/lib389/changelog.py @@ -16,6 +16,7 @@ from lib389 import DirSrv, Entry, InvalidArgumentError from lib389._mapped_object import DSLdapObject from lib389.utils import ds_is_older + class Changelog5(DSLdapObject): """Represents the Directory Server changelog. This is used for replication. Only one changelog is needed for every server. @@ -25,9 +26,9 @@ class Changelog5(DSLdapObject): """ def __init__(self, instance, dn='cn=changelog5,cn=config'): - super(Changelog5,self).__init__(instance, dn) + super(Changelog5, self).__init__(instance, dn) self._rdn_attribute = 'cn' - self._must_attributes = [ 'cn', 'nsslapd-changelogdir' ] + self._must_attributes = ['cn', 'nsslapd-changelogdir'] self._create_objectclasses = [ 'top', 'nsChangelogConfig', @@ -37,7 +38,7 @@ class Changelog5(DSLdapObject): 'top', 'extensibleobject', ] - self._protected = True + self._protected = False def set_max_entries(self, value): """Configure the max entries the changelog can hold. diff --git a/src/lib389/lib389/cli_conf/backend.py b/src/lib389/lib389/cli_conf/backend.py index 6602943..85ecfa4 100644 --- a/src/lib389/lib389/cli_conf/backend.py +++ b/src/lib389/lib389/cli_conf/backend.py @@ -163,12 +163,12 @@ def create_parser(subparsers): help="Specifies the filename of the input LDIF files." "When multiple files are imported, they are imported in the order" "they are specified on the command line.") - import_parser.add_argument('-c', '--chunks_size', type=int, + import_parser.add_argument('-c', '--chunks-size', type=int, help="The number of chunks to have during the import operation.") import_parser.add_argument('-E', '--encrypted', action='store_true', help="Decrypts encrypted data during export. This option is used only" "if database encryption is enabled.") - import_parser.add_argument('-g', '--gen_uniq_id', + import_parser.add_argument('-g', '--gen-uniq-id', help="Generate a unique id. Type none for no unique ID to be generated" "and deterministic for the generated unique ID to be name-based." "By default, a time-based unique ID is generated." @@ -176,11 +176,11 @@ def create_parser(subparsers): "it is also possible to specify the namespace for the server to use." "namespaceId is a string of characters" "in the format 00-xxxxxxxx-xxxxxxxx-xxxxxxxx-xxxxxxxx.") - import_parser.add_argument('-O', '--only_core', action='store_true', + import_parser.add_argument('-O', '--only-core', action='store_true', help="Requests that only the core database is created without attribute indexes.") - import_parser.add_argument('-s', '--include_suffixes', nargs='+', + import_parser.add_argument('-s', '--include-suffixes', nargs='+', help="Specifies the suffixes or the subtrees to be included.") - import_parser.add_argument('-x', '--exclude_suffixes', nargs='+', + import_parser.add_argument('-x', '--exclude-suffixes', nargs='+', help="Specifies the suffixes to be excluded.") export_parser = subcommands.add_parser('export', help='do an online export of the suffix') @@ -190,21 +190,21 @@ def create_parser(subparsers): export_parser.add_argument('-l', '--ldif', help="Gives the filename of the output LDIF file." "If more than one are specified, use a space as a separator") - export_parser.add_argument('-C', '--use_id2entry', action='store_true', help="Uses only the main database file.") + export_parser.add_argument('-C', '--use-id2entry', action='store_true', help="Uses only the main database file.") export_parser.add_argument('-E', '--encrypted', action='store_true', help="""Decrypts encrypted data during export. This option is used only if database encryption is enabled.""") - export_parser.add_argument('-m', '--min_base64', action='store_true', + export_parser.add_argument('-m', '--min-base64', action='store_true', help="Sets minimal base-64 encoding.") - export_parser.add_argument('-N', '--no_seq_num', action='store_true', + export_parser.add_argument('-N', '--no-seq-num', action='store_true', help="Enables you to suppress printing the sequence number.") export_parser.add_argument('-r', '--replication', action='store_true', help="Exports the information required to initialize a replica when the LDIF is imported") - export_parser.add_argument('-u', '--no_dump_uniq_id', action='store_true', + export_parser.add_argument('-u', '--no-dump-uniq-id', action='store_true', help="Requests that the unique ID is not exported.") - export_parser.add_argument('-U', '--not_folded', action='store_true', + export_parser.add_argument('-U', '--not-folded', action='store_true', help="Requests that the output LDIF is not folded.") - export_parser.add_argument('-s', '--include_suffixes', nargs='+', + export_parser.add_argument('-s', '--include-suffixes', nargs='+', help="Specifies the suffixes or the subtrees to be included.") - export_parser.add_argument('-x', '--exclude_suffixes', nargs='+', + export_parser.add_argument('-x', '--exclude-suffixes', nargs='+', help="Specifies the suffixes to be excluded.") diff --git a/src/lib389/lib389/cli_conf/pwpolicy.py b/src/lib389/lib389/cli_conf/pwpolicy.py index 2ec7c98..bbbd060 100644 --- a/src/lib389/lib389/cli_conf/pwpolicy.py +++ b/src/lib389/lib389/cli_conf/pwpolicy.py @@ -147,7 +147,7 @@ def list_policies(inst, basedn, log, args): result += "%s (%s)\n" % (entrydn, policy_type.lower()) if args.json: - return print(json.dumps(result)) + print(json.dumps(result)) else: log.info(result) diff --git a/src/lib389/lib389/cli_conf/replication.py b/src/lib389/lib389/cli_conf/replication.py new file mode 100644 index 0000000..ac34d50 --- /dev/null +++ b/src/lib389/lib389/cli_conf/replication.py @@ -0,0 +1,1046 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2018 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + +import json +import ldap +from getpass import getpass +from lib389._constants import * +from lib389.changelog import Changelog5 +from lib389.utils import is_a_dn +from lib389.replica import Replicas, BootstrapReplicationManager +from lib389.tasks import CleanAllRUVTask, AbortCleanAllRUVTask + + +arg_to_attr = { + # replica config + 'replica_id': 'nsds5replicaid', + 'repl_purge_delay': 'nsds5replicapurgedelay', + 'repl_tombstone_purge_interval': 'nsds5replicatombstonepurgeinterval', + 'repl_fast_tombstone_purging': 'nsds5ReplicaPreciseTombstonePurging', + 'repl_bind_group': 'nsds5replicabinddngroup', + 'repl_bind_group_interval': 'nsds5replicabinddngroupcheckinterval', + 'repl_protocol_timeout': 'nsds5replicaprotocoltimeout', + 'repl_backoff_min': 'nsds5replicabackoffmin', + 'repl_backoff_max': 'nsds5replicabackoffmax', + 'repl_release_timeout': 'nsds5replicareleasetimeout', + # Changelog + 'max_entries': 'nsslapd-changelogmaxentries', + 'max_age': 'nsslapd-changelogmaxage', + 'compact_interval': 'nsslapd-changelogcompactdb-interval', + 'trim_interval': 'nsslapd-changelogtrim-interval', + 'encrypt_algo': 'nsslapd-encryptionalgorithm', + 'encrypt_key': 'nssymmetrickey', + # Agreement + 'host': 'nsds5replicahost', + 'port': 'nsds5replicaport', + 'conn_protocol': 'nsds5replicatransportinfo', + 'bind_dn': 'nsds5replicabinddn', + 'bind_passwd': 'nsds5replicacredentials', + 'bind_method': 'nsds5replicabindmethod', + 'frac_list': 'nsds5replicatedattributelist', + 'frac_list_total': 'nsds5replicatedattributelisttotal', + 'strip_list': 'nsds5replicastripattrs', + 'schedule': 'nsds5replicaupdateschedule', + 'conn_timeout': 'nsds5replicatimeout', + 'protocol_timeout': 'nsds5replicaprotocoltimeout', + 'wait_async_results': 'nsds5replicawaitforasyncresults', + 'busy_wait_time': 'nsds5replicabusywaittime', + 'session_pause_time': 'nsds5replicaSessionPauseTime', + 'flow_control_window': 'nsds5replicaflowcontrolwindow', + 'flow_control_pause': 'nsds5replicaflowcontrolpause', + # Additional Winsync Agmt attrs + 'win_subtree': 'nsds7windowsreplicasubtree', + 'ds_subtree': 'nsds7directoryreplicasubtree', + 'sync_users': 'nsds7newwinusersyncenabled', + 'sync_groups': 'nsds7newwingroupsyncenabled', + 'win_domain': 'nsds7windowsDomain', + 'sync_interval': 'winsyncinterval', + 'one_way_sync': 'onewaysync', + 'move_action': 'winsyncmoveAction', + 'ds_filter': 'winsyncdirectoryfilter', + 'win_filter': 'winsyncwindowsfilter', + 'subtree_pair': 'winSyncSubtreePair' + } + + +def get_agmt(inst, args, winsync=False): + agmt_name = args.AGMT_NAME[0] + replicas = Replicas(inst) + replica = replicas.get(args.suffix) + agmts = replica.get_agreements(winsync=winsync) + try: + agmt = agmts.get(agmt_name) + except ldap.NO_SUCH_OBJECT: + raise ValueError("Could not find the agreement \"{}\" for suffix \"{}\"".format(agmt_name, args.suffix)) + return agmt + + +def _args_to_attrs(args): + attrs = {} + for arg in vars(args): + val = getattr(args, arg) + if arg in arg_to_attr and val is not None: + attrs[arg_to_attr[arg]] = val + return attrs + + +# +# Replica config +# +def enable_replication(inst, basedn, log, args): + repl_root = args.suffix + role = args.role.lower() + rid = args.replica_id + + if role == "master": + repl_type = '3' + repl_flag = '1' + elif role == "hub": + repl_type = '2' + repl_flag = '1' + elif role == "consumer": + repl_type = '2' + repl_flag = '0' + else: + # error - unknown type + raise ValueError("Unknown replication role ({}), you must use \"master\", \"hub\", or \"consumer\"".format(role)) + + # Start the propeties and update them as needed + repl_properties = { + 'cn': 'replica', + 'nsDS5ReplicaRoot': repl_root, + 'nsDS5Flags': repl_flag, + 'nsDS5ReplicaType': repl_type, + } + + # Validate master settings + if role == "master": + # Do we have a rid? + if not args.replica_id or args.replica_id is None: + # Error, master needs a rid TODO + raise ValueError('You must specify the replica ID (--replica-id) when enabling a \"master\" replica') + + # is it a number? + try: + rid_num = int(rid) + except ValueError: + raise ValueError("--rid expects a number between 1 and 65534") + + # Is it in range? + if rid_num < 1 or rid_num > 65534: + raise ValueError("--replica-id expects a number between 1 and 65534") + + # rid is good add it to the props + repl_properties['nsDS5ReplicaId'] = rid + + # Bind DN or Bind DN Group? + if args.bind_group_dn: + repl_properties['nsDS5ReplicaBindDNGroup'] = args.bind_group_dn + elif args.bind_dn: + repl_properties['nsDS5ReplicaBindDN'] = args.bind_dn + + # First create the changelog + cl = Changelog5(inst) + try: + cl.create(properties={ + 'cn': 'changelog5', + 'nsslapd-changelogdir': inst.get_changelog_dir() + }) + except ldap.ALREADY_EXISTS: + pass + + # Finally enable replication + replicas = Replicas(inst) + try: + replicas.create(properties=repl_properties) + except ldap.ALREADY_EXISTS: + raise ValueError("Replication is already enabled for this suffix") + + print("Replication successfully enabled for \"{}\"".format(repl_root)) + + +def disable_replication(inst, basedn, log, args): + replicas = Replicas(inst) + try: + replica = replicas.get(args.suffix) + replica.delete() + except ldap.NO_SUCH_OBJECT: + raise ValueError("Backend \"{}\" is not enabled for replication".format(args.suffix)) + print("Replication disabled for \"{}\"".format(args.suffix)) + + +def promote_replica(inst, basedn, log, args): + replicas = Replicas(inst) + replica = replicas.get(args.suffix) + role = args.newrole.lower() + + if role == 'master': + newrole = ReplicaRole.MASTER + if args.rreplica_idid is None: + raise ValueError("You need to provide a replica ID (--replica-id) to promote replica to a master") + elif role == 'hub': + newrole = ReplicaRole.HUB + else: + raise ValueError("Invalid role ({}), you must use either \"master\" or \"hub\"".format(role)) + + replica.promote(newrole, binddn=args.bind_dn, binddn_group=args.bind_group_dn, rid=args.replica_id) + print("Successfully promoted replica to \"{}\"".format(role)) + + +def demote_replica(inst, basedn, log, args): + replicas = Replicas(inst) + replica = replicas.get(args.suffix) + role = args.newrole.lower() + + if role == 'hub': + newrole = ReplicaRole.HUB + elif role == 'consumer': + newrole = ReplicaRole.CONSUMER + else: + raise ValueError("Invalid role ({}), you must use either \"hub\" or \"consumer\"".format(role)) + + replica.demote(newrole) + print("Successfully demoted replica to \"{}\"".format(role)) + + +def get_repl_config(inst, basedn, log, args): + replicas = Replicas(inst) + replica = replicas.get(args.suffix) + if args and args.json: + print(replica.get_all_attrs_json()) + else: + log.info(replica.display()) + + +def set_repl_config(inst, basedn, log, args): + replicas = Replicas(inst) + replica = replicas.get(args.suffix) + attrs = _args_to_attrs(args) + op_count = 0 + + # Add supplier DNs + if args.repl_add_bind_dn is not None: + if not is_a_dn(repl_add_bind_dn): + raise ValueError("The replica bind DN is not a valid DN") + replica.add('nsds5ReplicaBindDN', args.repl_add_bind_dn) + op_count += 1 + + # Remove supplier DNs + if args.repl_del_bind_dn is not None: + replica.remove('nsds5ReplicaBindDN', args.repl_del_bind_dn) + op_count += 1 + + # Add referral + if args.repl_add_ref is not None: + replica.add('nsDS5ReplicaReferral', args.repl_add_ref) + op_count += 1 + + # Remove referral + if args.repl_del_ref is not None: + replica.remove('nsDS5ReplicaReferral', args.repl_del_ref) + op_count += 1 + + # Handle the rest of the changes that use mod_replace + modlist = [] + for attr, value in attrs.items(): + modlist.append((attr, value)) + if len(modlist) > 0: + replica.replace_many(*modlist) + elif op_count == 0: + raise ValueError("There are no changes to set in the replica") + print("Successfully updated replication configuration") + + +def create_cl(inst, basedn, log, args): + cl = Changelog5(inst) + try: + cl.create(properties={ + 'cn': 'changelog5', + 'nsslapd-changelogdir': inst.get_changelog_dir() + }) + except ldap.ALREADY_EXISTS: + raise ValueError("Changelog already exists") + print("Successfully created replication changelog") + + +def delete_cl(inst, basedn, log, args): + cl = Changelog5(inst) + try: + cl.delete() + except ldap.NO_SUCH_OBJECT: + raise ValueError("There is no changelog to delete") + print("Successfully deleted replication changelog") + + +def set_cl(inst, basedn, log, args): + cl = Changelog5(inst) + attrs = _args_to_attrs(args) + modlist = [] + for attr, value in attrs.items(): + modlist.append((attr, value)) + if len(modlist) > 0: + cl.replace_many(*modlist) + else: + raise ValueError("There are no changes to set for the replication changelog") + print("Successfully updated replication changelog") + + +def get_cl(inst, basedn, log, args): + cl = Changelog5(inst) + if args and args.json: + print(cl.get_all_attrs_json()) + else: + log.info(cl.display()) + + +def create_repl_manager(inst, basedn, log, args): + manager_cn = "replication manager" + repl_manager_password = "" + repl_manager_password_confirm = "" + + if args.name: + manager_cn = args.name + + if is_a_dn(manager_cn): + # A full DN was provided, make sure it uses "cn" for the RDN + if manager_cn.split("=", 1)[0].lower() != "cn": + raise ValueError("Replication manager DN must use \"cn\" for the rdn attribute") + manager_dn = manager_cn + else: + manager_dn = "cn={},cn=config".format(manager_cn) + + if args.passwd: + repl_manager_password = args.passwd + else: + # Prompt for password + while 1: + while repl_manager_password == "": + repl_manager_password = getpass("Enter replication manager password: ") + while repl_manager_password_confirm == "": + repl_manager_password_confirm = getpass("Confirm replication manager password: ") + if repl_manager_password_confirm == repl_manager_password: + break + else: + print("Passwords do not match!\n") + repl_manager_password = "" + repl_manager_password_confirm = "" + + manager = BootstrapReplicationManager(inst, dn=manager_dn) + try: + manager.create(properties={ + 'cn': manager_cn, + 'userPassword': repl_manager_password + }) + print ("Successfully created replication manager: " + manager_dn) + except ldap.ALREADY_EXISTS: + log.info("Replication Manager ({}) already exists".format(manager_dn)) + + +def del_repl_manager(inst, basedn, log, args): + if is_a_dn(agmt.name): + manager_dn = args.name + else: + manager_dn = "cn={},cn=config".format(args.name) + manager = BootstrapReplicationManager(inst, dn=manager_dn) + manager.delete() + print("Successfully deleted replication manager: " + manager_dn) + + +# +# Agreements +# +def list_agmts(inst, basedn, log, args): + # List regular DS agreements + replicas = Replicas(inst) + replica = replicas.get(args.suffix) + agmts = replica.get_agreements().list() + + result = {"type": "list", "items": []} + for agmt in agmts: + if args.json: + entry = agmt.get_all_attrs_json() + # Append decoded json object, because we are going to dump it later + result['items'].append(json.loads(entry)) + else: + print(agmt.display()) + if args.json: + print(json.dumps(result)) + + +def add_agmt(inst, basedn, log, args): + repl_root = args.suffix + bind_method = args.bind_method.lower() + replicas = Replicas(inst) + replica = replicas.get(args.suffix) + agmts = replica.get_agreements() + + # Process fractional settings + frac_list = None + if args.frac_list: + frac_list = "(objectclass=*) $ EXCLUDE" + for attr in args.frac_list.split(): + frac_list += " " + attr + + frac_total_list = None + if args.frac_list_total: + frac_total_list = "(objectclass=*) $ EXCLUDE" + for attr in args.frac_list_total.split(): + frac_total_list += " " + attr + + # Required properties + properties = { + 'cn': args.AGMT_NAME[0], + 'nsDS5ReplicaRoot': repl_root, + 'description': args.AGMT_NAME[0], + 'nsDS5ReplicaHost': args.host, + 'nsDS5ReplicaPort': args.port, + 'nsDS5ReplicaBindMethod': bind_method, + 'nsDS5ReplicaTransportInfo': args.conn_protocol + } + + # Add optional properties + if args.bind_dn is not None: + if not is_a_dn(args.bind_dn): + raise ValueError("The replica bind DN is not a valid DN") + properties['nsDS5ReplicaBindDN'] = args.bind_dn + if args.bind_passwd is not None: + properties['nsDS5ReplicaCredentials'] = args.bind_passwd + if args.schedule is not None: + properties['nsds5replicaupdateschedule'] = args.schedule + if frac_list is not None: + properties['nsds5replicatedattributelist'] = frac_list + if frac_total_list is not None: + properties['nsds5replicatedattributelisttotal'] = frac_total_list + if args.strip_list is not None: + properties['nsds5replicastripattrs'] = args.strip_list + + # We do need the bind dn and credentials for none-sasl bind methods + if (bind_method == 'simple' or 'sslclientauth') and (args.bind_dn is None or args.bind_passwd is None): + raise ValueError("You need to set the bind dn (--bind-dn) and the password (--bind-passwd) for bind method ({})".format(bind_method)) + + # Create the agmt + try: + agmts.create(properties=properties) + except ldap.ALREADY_EXISTS: + raise ValueError("A replication agreement with the same name already exists") + + print("Successfully created replication agreement \"{}\"".format(args.AGMT_NAME[0])) + if args.init: + init_agmt(inst, basedn, log, args) + + +def delete_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args) + agmt.delete() + print("Agreement has been successfully deleted") + + +def enable_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args) + agmt.resume() + print("Agreement has been enabled") + + +def disable_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args) + agmt.pause() + print("Agreement has been disabled") + + +def init_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args) + agmt.begin_reinit() + print("Agreement initialization started...") + + +def check_init_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args) + (done, inprogress, error) = agmt.check_reinit() + status = "Unknown" + if done: + status = "Agreement successfully initialized." + elif inprogress: + status = "Agreement initialization in progress." + elif error: + status = "Agreement initialization failed." + if args.json: + print(json.dumps(status)) + else: + print(status) + + +def set_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args) + attrs = _args_to_attrs(args) + modlist = [] + for attr, value in attrs.items(): + modlist.append((attr, value)) + if len(modlist) > 0: + agmt.replace_many(*modlist) + else: + raise ValueError("There are no changes to set in the agreement") + print("Successfully updated agreement") + + +def get_repl_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args) + if args.json: + print(agmt.get_all_attrs_json()) + else: + print(agmt.display()) + + +def poke_agmt(inst, basedn, log, args): + # Send updates now + agmt = get_agmt(inst, args) + agmt.pause() + agmt.resume() + print("Agreement has been poked") + + +def get_agmt_status(inst, basedn, log, args): + agmt = get_agmt(inst, args) + if args.bind_dn is not None and args.bind_passwd is None: + args.bind_passwd = "" + while args.bind_passwd == "": + args.bind_passwd = getpass("Enter password for \"{}\": ".format(args.bind_dn)) + status = agmt.status(use_json=args.json, binddn=args.bind_dn, bindpw=args.bind_passwd) + print(agmt, status) + + +# +# Winsync agreement specfic functions +# +def list_winsync_agmts(inst, basedn, log, args): + # List regular DS agreements + replicas = Replicas(inst) + replica = replicas.get(args.suffix) + agmts = replica.get_agreements(winsync=True).list() + + result = {"type": "list", "items": []} + for agmt in agmts: + if args.json: + entry = agmt.get_all_attrs_json() + # Append decoded json object, because we are going to dump it later + result['items'].append(json.loads(entry)) + else: + print(agmt.display()) + if args.json: + print(json.dumps(result)) + + +def add_winsync_agmt(inst, basedn, log, args): + replicas = Replicas(inst) + replica = replicas.get(args.suffix) + agmts = replica.get_agreements(winsync=True) + + # Process fractional settings + frac_list = None + if args.frac_list: + frac_list = "(objectclass=*) $ EXCLUDE" + for attr in args.frac_list.split(): + frac_list += " " + attr + + if not is_a_dn(args.bind_dn): + raise ValueError("The replica bind DN is not a valid DN") + + # Required properties + properties = { + 'cn': args.AGMT_NAME[0], + 'nsDS5ReplicaRoot': args.suffix, + 'description': args.AGMT_NAME[0], + 'nsDS5ReplicaHost': args.host, + 'nsDS5ReplicaPort': args.port, + 'nsDS5ReplicaTransportInfo': args.conn_protocol, + 'nsDS5ReplicaBindDN': args.bind_dn, + 'nsDS5ReplicaCredentials': args.bind_passwd, + 'nsds7windowsreplicasubtree': args.win_subtree, + 'nsds7directoryreplicasubtree': args.ds_subtree, + 'nsds7windowsDomain': args.win_domain, + } + + # Add optional properties + if args.sync_users is not None: + properties['nsds7newwinusersyncenabled'] = args.sync_users + if args.sync_groups is not None: + properties['nsds7newwingroupsyncenabled'] = args.sync_groups + if args.sync_interval is not None: + properties['winsyncinterval'] = args.sync_interval + if args.one_way_sync is not None: + properties['onewaysync'] = args.one_way_sync + if args.move_action is not None: + properties['winsyncmoveAction'] = args.move_action + if args.ds_filter is not None: + properties['winsyncdirectoryfilter'] = args.ds_filter + if args.win_filter is not None: + properties['winsyncwindowsfilter'] = args.win_filter + if args.schedule is not None: + properties['nsds5replicaupdateschedule'] = args.schedule + if frac_list is not None: + properties['nsds5replicatedattributelist'] = frac_list + + # Create the agmt + try: + agmts.create(properties=properties) + except ldap.ALREADY_EXISTS: + raise ValueError("A replication agreement with the same name already exists") + + print("Successfully created winsync replication agreement \"{}\"".format(args.AGMT_NAME[0])) + if args.init: + init_winsync_agmt(inst, basedn, log, args) + + +def delete_winsync_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args, winsync=True) + agmt.delete() + print("Agreement has been successfully deleted") + + +def set_winsync_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args, winsync=True) + + attrs = _args_to_attrs(args) + modlist = [] + for attr, value in attrs.items(): + modlist.append((attr, value)) + if len(modlist) > 0: + agmt.replace_many(*modlist) + else: + raise ValueError("There are no changes to set in the agreement") + print("Successfully updated agreement") + + +def enable_winsync_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args, winsync=True) + agmt.resume() + print("Agreement has been enabled") + + +def disable_winsync_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args, winsync=True) + agmt.pause() + print("Agreement has been disabled") + + +def init_winsync_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args, winsync=True) + agmt.begin_reinit() + print("Agreement initialization started...") + + +def check_winsync_init_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args, winsync=True) + (done, inprogress, error) = agmt.check_reinit() + status = "Unknown" + if done: + status = "Agreement successfully initialized." + elif inprogress: + status = "Agreement initialization in progress." + elif error: + status = "Agreement initialization failed." + if args.json: + print(json.dumps(status)) + else: + print(status) + + +def get_winsync_agmt(inst, basedn, log, args): + agmt = get_agmt(inst, args, winsync=True) + if args.json: + print(agmt.get_all_attrs_json()) + else: + print(agmt.display()) + + +def poke_winsync_agmt(inst, basedn, log, args): + # Send updates now + agmt = get_agmt(inst, args, winsync=True) + agmt.pause() + agmt.resume() + print("Agreement has been poked") + + +def get_winsync_agmt_status(inst, basedn, log, args): + agmt = get_agmt(inst, args, winsync=True) + status = agmt.status(winsync=True, use_json=args.json) + print(agmt, status) + + +# +# Tasks +# +def run_cleanallruv(inst, basedn, log, args): + properties = {'replica-base-dn': args.suffix, + 'replica-id': args.replica_id} + if args.force_cleaning: + properties['replica-force-cleaning'] = args.force_cleaning + clean_task = CleanAllRUVTask(inst) + clean_task.create(properties=properties) + + +def abort_cleanallruv(inst, basedn, log, args): + properties = {'replica-base-dn': args.suffix, + 'replica-id': args.replica_id} + if args.certify: + properties['replica-certify-all'] = args.certify + clean_task = AbortCleanAllRUVTask(inst) + clean_task.create(properties=properties) + + +def create_parser(subparsers): + + ############################################ + # Replication Configuration + ############################################ + + repl_parser = subparsers.add_parser('replication', help='Configure replication for a suffix') + repl_subcommands = repl_parser.add_subparsers(help='Replication Configuration') + + repl_enable_parser = repl_subcommands.add_parser('enable', help='Enable replication for a suffix') + repl_enable_parser.set_defaults(func=enable_replication) + repl_enable_parser.add_argument('--suffix', required=True, help='The DN of the suffix to be enabled for replication') + repl_enable_parser.add_argument('--role', required=True, help="The Replication role: \"master\", \"hub\", or \"consumer\"") + repl_enable_parser.add_argument('--replica-id', help="The replication identifier for a \"master\". Values range from 1 - 65534") + repl_enable_parser.add_argument('--bind-group-dn', help="A group entry DN containing members that are \"bind/supplier\" DNs") + repl_enable_parser.add_argument('--bind-dn', help="The Bind or Supplier DN that can make replication updates") + + repl_disable_parser = repl_subcommands.add_parser('disable', help='Disable replication for a suffix') + repl_disable_parser.set_defaults(func=disable_replication) + repl_disable_parser.add_argument('--suffix', required=True, help='The DN of the suffix to have replication disabled') + + repl_promote_parser = repl_subcommands.add_parser('promote', help='Promte replica to a Hub or Master') + repl_promote_parser.set_defaults(func=promote_replica) + repl_promote_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix to promote") + repl_promote_parser.add_argument('--newrole', required=True, help='Promote this replica to a \"hub\" or \"master\"') + repl_promote_parser.add_argument('--replica-id', help="The replication identifier for a \"master\". Values range from 1 - 65534") + repl_promote_parser.add_argument('--bind-group-dn', help="A group entry DN containing members that are \"bind/supplier\" DNs") + repl_promote_parser.add_argument('--bind-dn', help="The Bind or Supplier DN that can make replication updates") + + repl_add_manager_parser = repl_subcommands.add_parser('create-manager', help='Create a replication manager entry') + repl_add_manager_parser.set_defaults(func=create_repl_manager) + repl_add_manager_parser.add_argument('--name', help="The NAME of the new replication manager entry under cn=config: \"cn=NAME,cn=config\"") + repl_add_manager_parser.add_argument('--passwd', help="Password for replication manager. If not provided, you will be prompted for the password") + + repl_del_manager_parser = repl_subcommands.add_parser('delete-manager', help='Delete a replication manager entry') + repl_del_manager_parser.set_defaults(func=del_repl_manager) + repl_del_manager_parser.add_argument('--name', help="The NAME of the replication manager entry under cn=config: \"cn=NAME,cn=config\"") + + repl_demote_parser = repl_subcommands.add_parser('demote', help='Demote replica to a Hub or Consumer') + repl_demote_parser.set_defaults(func=demote_replica) + repl_demote_parser.add_argument('--suffix', required=True, help="Promte this replica to a \"hub\" or \"consumer\"") + repl_demote_parser.add_argument('--newrole', required=True, help="The Replication role: \"hub\", or \"consumer\"") + + repl_get_parser = repl_subcommands.add_parser('get', help='Get replication configuration') + repl_get_parser.set_defaults(func=get_repl_config) + repl_get_parser.add_argument('--suffix', required=True, help='Get the replication configuration for this suffix DN') + + repl_create_cl = repl_subcommands.add_parser('create-changelog', help='Create the replication changelog') + repl_create_cl.set_defaults(func=create_cl) + + repl_delete_cl = repl_subcommands.add_parser('delete-changelog', help='Delete the replication changelog. This will invalidate any existing replication agreements') + repl_delete_cl.set_defaults(func=delete_cl) + + repl_set_cl = repl_subcommands.add_parser('set-changelog', help='Delete the replication changelog. This will invalidate any existing replication agreements') + repl_set_cl.set_defaults(func=set_cl) + repl_set_cl.add_argument('--max-entries', help="The maximum number of entries to get in the replication changelog") + repl_set_cl.add_argument('--max-age', help="The maximum age of a replication changelog entry") + repl_set_cl.add_argument('--compact-interval', help="The replication changelog compaction interval") + repl_set_cl.add_argument('--trim-interval', help="The interval to check if the replication changelog can be trimmed") + repl_set_cl.add_argument('--encrypt-algo', help="The encryption algorithm used to encrypt the replication changelog content. Requires that TLS is enabled in the server") + repl_set_cl.add_argument('--encrypt-key', help="The symmetric key for the replication changelog encryption") + + repl_get_cl = repl_subcommands.add_parser('get-changelog', help='Delete the replication changelog. This will invalidate any existing replication agreements') + repl_get_cl.set_defaults(func=get_cl) + + repl_set_parser = repl_subcommands.add_parser('set', help='Set an attribute in the replication configuration') + repl_set_parser.set_defaults(func=set_repl_config) + repl_set_parser.add_argument('--suffix', required=True, help='The DN of the replication suffix') + repl_set_parser.add_argument('--replica-id', help="The Replication Identifier number") + repl_set_parser.add_argument('--replica-role', help="The Replication role: master, hub, or consumer") + + repl_set_parser.add_argument('--repl-add-bind-dn', help="Add a bind (supplier) DN") + repl_set_parser.add_argument('--repl-del-bind-dn', help="Remove a bind (supplier) DN") + repl_set_parser.add_argument('--repl-add-ref', help="Add a replication referral (for consumers only)") + repl_set_parser.add_argument('--repl-del-ref', help="Remove a replication referral (for conusmers only)") + repl_set_parser.add_argument('--repl-purge-delay', help="The replication purge delay") + repl_set_parser.add_argument('--repl-tombstone-purge-interval', help="The interval in seconds to check for tombstones that can be purged") + repl_set_parser.add_argument('--repl-fast-tombstone-purging', help="Set to \"on\" to improve tombstone purging performance") + repl_set_parser.add_argument('--repl-bind-group', help="A group entry DN containing members that are \"bind/supplier\" DNs") + repl_set_parser.add_argument('--repl-bind-group-interval', help="An interval in seconds to check if the bind group has been updated") + repl_set_parser.add_argument('--repl-protocol-timeout', help="A timeout in seconds on how long to wait before stopping " + "replication when the server is under load") + repl_set_parser.add_argument('--repl-backoff-max', help="The maximum time in seconds a replication agreement should stay in a backoff state " + "while waiting to acquire the consumer. Default is 300 seconds") + repl_set_parser.add_argument('--repl-backoff-min', help="The starting time in seconds a replication agreement should stay in a backoff state " + "while waiting to acquire the consumer. Default is 3 seconds") + repl_set_parser.add_argument('--repl-release-timeout', help="A timeout in seconds a replication master should send " + "updates before it yields its replication session") + + ############################################ + # Replication Agmts + ############################################ + + agmt_parser = subparsers.add_parser('repl-agmt', help='Manage replication agreements') + agmt_subcommands = agmt_parser.add_subparsers(help='Replication Agreement Configuration') + + # List + agmt_list_parser = agmt_subcommands.add_parser('list', help='List all the replication agreements') + agmt_list_parser.set_defaults(func=list_agmts) + agmt_list_parser.add_argument('--suffix', required=True, help='The DN of the suffix to look up replication agreements') + agmt_list_parser.add_argument('--entry', help='Return the entire entry for each agreement') + + # Enable + agmt_enable_parser = agmt_subcommands.add_parser('enable', help='Enable replication agreement') + agmt_enable_parser.set_defaults(func=enable_agmt) + agmt_enable_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement') + agmt_enable_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix") + + # Disable + agmt_disable_parser = agmt_subcommands.add_parser('disable', help='Disable replication agreement') + agmt_disable_parser.set_defaults(func=disable_agmt) + agmt_disable_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement') + agmt_disable_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix") + + # Initialize + agmt_init_parser = agmt_subcommands.add_parser('init', help='Initialize replication agreement') + agmt_init_parser.set_defaults(func=init_agmt) + agmt_init_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement') + agmt_init_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix") + + # Check Initialization progress + agmt_check_init_parser = agmt_subcommands.add_parser('init-status', help='Check the agreement initialization status') + agmt_check_init_parser.set_defaults(func=check_init_agmt) + agmt_check_init_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement') + agmt_check_init_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix") + + # Send Updates Now + agmt_poke_parser = agmt_subcommands.add_parser('poke', help='Trigger replication to send updates now') + agmt_poke_parser.set_defaults(func=poke_agmt) + agmt_poke_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement') + agmt_poke_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix") + + # Status + agmt_status_parser = agmt_subcommands.add_parser('status', help='Get the current status of the replication agreement') + agmt_status_parser.set_defaults(func=get_agmt_status) + agmt_status_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement') + agmt_status_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix") + agmt_status_parser.add_argument('--bind-dn', help="Set the DN to bind to the consumer") + agmt_status_parser.add_argument('--bind-passwd', help="The password for the bind DN") + + # Delete + agmt_del_parser = agmt_subcommands.add_parser('delete', help='Delete replication agreement') + agmt_del_parser.set_defaults(func=delete_agmt) + agmt_del_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement') + agmt_del_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix") + + # Create + agmt_add_parser = agmt_subcommands.add_parser('create', help='Initialize replication agreement') + agmt_add_parser.set_defaults(func=add_agmt) + agmt_add_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement') + agmt_add_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix") + agmt_add_parser.add_argument('--host', required=True, help="The hostname of the remote replica") + agmt_add_parser.add_argument('--port', required=True, help="The port number of the remote replica") + agmt_add_parser.add_argument('--conn-protocol', required=True, help="The replication connection protocol: LDAP, LDAPS, or StartTLS") + agmt_add_parser.add_argument('--bind-dn', help="The Bind DN the agreement uses to authenticate to the replica") + agmt_add_parser.add_argument('--bind-passwd', help="The credentials for the Bind DN") + agmt_add_parser.add_argument('--bind-method', required=True, help="The bind method: \"SIMPLE\", \"SSLCLIENTAUTH\", \"SASL/DIGEST\", or \"SASL/GSSAPI\"") + agmt_add_parser.add_argument('--frac-list', help="List of attributes to NOT replicate to the consumer during incremental updates") + agmt_add_parser.add_argument('--frac-list-total', help="List of attributes to NOT replicate during a total initialization") + agmt_add_parser.add_argument('--strip-list', help="A list of attributes that are removed from updates only if the event " + "would otherwise be empty. Typically this is set to \"modifiersname\" and \"modifytimestmap\"") + agmt_add_parser.add_argument('--schedule', help="Sets the replication update schedule: 'HHMM-HHMM DDDDDDD' D = 0-6 (Sunday - Saturday).") + agmt_add_parser.add_argument('--conn-timeout', help="The timeout used for replicaton connections") + agmt_add_parser.add_argument('--protocol-timeout', help="A timeout in seconds on how long to wait before stopping " + "replication when the server is under load") + agmt_add_parser.add_argument('--wait-async-results', help="The amount of time in milliseconds the server waits if " + "the consumer is not ready before resending data") + agmt_add_parser.add_argument('--busy-wait-time', help="The amount of time in seconds a supplier should wait after " + "a consumer sends back a busy response before making another " + "attempt to acquire access.") + agmt_add_parser.add_argument('--session-pause-time', help="The amount of time in seconds a supplier should wait between update sessions.") + agmt_add_parser.add_argument('--flow-control-window', help="Sets the maximum number of entries and updates sent by a supplier, which are not acknowledged by the consumer.") + agmt_add_parser.add_argument('--flow-control-pause', help="The time in milliseconds to pause after reaching the number of entries and updates set in \"--flow-control-window\"") + agmt_add_parser.add_argument('--init', action='store_true', default=False, help="Initialize the agreement after creating it.") + + # Set - Note can not use add's parent args because for "set" there are no "required=True" args + agmt_set_parser = agmt_subcommands.add_parser('set', help='Set an attribute in the replication agreement') + agmt_set_parser.set_defaults(func=set_agmt) + agmt_set_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement') + agmt_set_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix") + agmt_set_parser.add_argument('--host', help="The hostname of the remote replica") + agmt_set_parser.add_argument('--port', help="The port number of the remote replica") + agmt_set_parser.add_argument('--conn-protocol', help="The replication connection protocol: LDAP, LDAPS, or StartTLS") + agmt_set_parser.add_argument('--bind-dn', help="The Bind DN the agreement uses to authenticate to the replica") + agmt_set_parser.add_argument('--bind-passwd', help="The credentials for the Bind DN") + agmt_set_parser.add_argument('--bind-method', help="The bind method: \"SIMPLE\", \"SSLCLIENTAUTH\", \"SASL/DIGEST\", or \"SASL/GSSAPI\"") + agmt_set_parser.add_argument('--frac-list', help="List of attributes to NOT replicate to the consumer during incremental updates") + agmt_set_parser.add_argument('--frac-list-total', help="List of attributes to NOT replicate during a total initialization") + agmt_set_parser.add_argument('--strip-list', help="A list of attributes that are removed from updates only if the event " + "would otherwise be empty. Typically this is set to \"modifiersname\" and \"modifytimestmap\"") + agmt_set_parser.add_argument('--schedule', help="Sets the replication update schedule: 'HHMM-HHMM DDDDDDD' D = 0-6 (Sunday - Saturday).") + agmt_set_parser.add_argument('--conn-timeout', help="The timeout used for replicaton connections") + agmt_set_parser.add_argument('--protocol-timeout', help="A timeout in seconds on how long to wait before stopping " + "replication when the server is under load") + agmt_set_parser.add_argument('--wait-async-results', help="The amount of time in milliseconds the server waits if " + "the consumer is not ready before resending data") + agmt_set_parser.add_argument('--busy-wait-time', help="The amount of time in seconds a supplier should wait after " + "a consumer sends back a busy response before making another " + "attempt to acquire access.") + agmt_set_parser.add_argument('--session-pause-time', help="The amount of time in seconds a supplier should wait between update sessions.") + agmt_set_parser.add_argument('--flow-control-window', help="Sets the maximum number of entries and updates sent by a supplier, which are not acknowledged by the consumer.") + agmt_set_parser.add_argument('--flow-control-pause', help="The time in milliseconds to pause after reaching the number of entries and updates set in \"--flow-control-window\"") + + # Get + agmt_get_parser = agmt_subcommands.add_parser('get', help='Get replication configuration') + agmt_get_parser.set_defaults(func=get_repl_agmt) + agmt_get_parser.add_argument('AGMT_NAME', nargs=1, help='Get the replication configuration for this suffix DN') + agmt_get_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix") + + ############################################ + # Replication Winsync Agmts + ############################################ + + winsync_parser = subparsers.add_parser('repl-winsync-agmt', help='Manage Winsync Agreements') + winsync_agmt_subcommands = winsync_parser.add_subparsers(help='Replication Winsync Agreement Configuration') + + # List + winsync_agmt_list_parser = winsync_agmt_subcommands.add_parser('list', help='List all the replication winsync agreements') + winsync_agmt_list_parser.set_defaults(func=list_winsync_agmts) + winsync_agmt_list_parser.add_argument('--suffix', required=True, help='The DN of the suffix to look up replication winsync agreements') + + # Enable + winsync_agmt_enable_parser = winsync_agmt_subcommands.add_parser('enable', help='Enable replication winsync agreement') + winsync_agmt_enable_parser.set_defaults(func=enable_winsync_agmt) + winsync_agmt_enable_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement') + winsync_agmt_enable_parser.add_argument('--suffix', required=True, help="The DN of the replication winsync suffix") + + # Disable + winsync_agmt_disable_parser = winsync_agmt_subcommands.add_parser('disable', help='Disable replication winsync agreement') + winsync_agmt_disable_parser.set_defaults(func=disable_winsync_agmt) + winsync_agmt_disable_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement') + winsync_agmt_disable_parser.add_argument('--suffix', required=True, help="The DN of the replication winsync suffix") + + # Initialize + winsync_agmt_init_parser = winsync_agmt_subcommands.add_parser('init', help='Initialize replication winsync agreement') + winsync_agmt_init_parser.set_defaults(func=init_winsync_agmt) + winsync_agmt_init_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement') + winsync_agmt_init_parser.add_argument('--suffix', required=True, help="The DN of the replication winsync suffix") + + # Check Initialization progress + winsync_agmt_check_init_parser = winsync_agmt_subcommands.add_parser('init-status', help='Check the agreement initialization status') + winsync_agmt_check_init_parser.set_defaults(func=check_winsync_init_agmt) + winsync_agmt_check_init_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement') + winsync_agmt_check_init_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix") + + # Send Updates Now + winsync_agmt_poke_parser = winsync_agmt_subcommands.add_parser('poke', help='Trigger replication to send updates now') + winsync_agmt_poke_parser.set_defaults(func=poke_winsync_agmt) + winsync_agmt_poke_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement') + winsync_agmt_poke_parser.add_argument('--suffix', required=True, help="The DN of the replication winsync suffix") + + # Status + winsync_agmt_status_parser = winsync_agmt_subcommands.add_parser('status', help='Get the current status of the replication agreement') + winsync_agmt_status_parser.set_defaults(func=get_winsync_agmt_status) + winsync_agmt_status_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement') + winsync_agmt_status_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix") + + # Delete + winsync_agmt_del_parser = winsync_agmt_subcommands.add_parser('delete', help='Delete replication winsync agreement') + winsync_agmt_del_parser.set_defaults(func=delete_winsync_agmt) + winsync_agmt_del_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement') + winsync_agmt_del_parser.add_argument('--suffix', required=True, help="The DN of the replication winsync suffix") + + # Create + winsync_agmt_add_parser = winsync_agmt_subcommands.add_parser('create', help='Initialize replication winsync agreement') + winsync_agmt_add_parser.set_defaults(func=add_winsync_agmt) + winsync_agmt_add_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement') + winsync_agmt_add_parser.add_argument('--suffix', required=True, help="The DN of the replication winsync suffix") + winsync_agmt_add_parser.add_argument('--host', required=True, help="The hostname of the AD server") + winsync_agmt_add_parser.add_argument('--port', required=True, help="The port number of the AD server") + winsync_agmt_add_parser.add_argument('--conn-protocol', required=True, help="The replication winsync connection protocol: LDAP, LDAPS, or StartTLS") + winsync_agmt_add_parser.add_argument('--bind-dn', required=True, help="The Bind DN the agreement uses to authenticate to the AD Server") + winsync_agmt_add_parser.add_argument('--bind-passwd', required=True, help="The credentials for the Bind DN") + winsync_agmt_add_parser.add_argument('--frac-list', help="List of attributes to NOT replicate to the consumer during incremental updates") + winsync_agmt_add_parser.add_argument('--schedule', help="Sets the replication update schedule") + winsync_agmt_add_parser.add_argument('--win-subtree', required=True, help="The suffix of the AD Server") + winsync_agmt_add_parser.add_argument('--ds-subtree', required=True, help="The Directory Server suffix") + winsync_agmt_add_parser.add_argument('--win-domain', required=True, help="The AD Domain") + winsync_agmt_add_parser.add_argument('--sync-users', help="Synchronize Users between AD and DS") + winsync_agmt_add_parser.add_argument('--sync-groups', help="Synchronize Groups between AD and DS") + winsync_agmt_add_parser.add_argument('--sync-interval', help="The interval that DS checks AD for changes in entries") + winsync_agmt_add_parser.add_argument('--one-way-sync', help="Sets which direction to perform synchronization: \"toWindows\", \"fromWindows\", \"both\"") + winsync_agmt_add_parser.add_argument('--move-action', help="Sets instructions on how to handle moved or deleted entries: \"none\", \"unsync\", or \"delete\"") + winsync_agmt_add_parser.add_argument('--win-filter', help="Custom filter for finding users in AD Server") + winsync_agmt_add_parser.add_argument('--ds-filter', help="Custom filter for finding AD users in DS Server") + winsync_agmt_add_parser.add_argument('--subtree-pair', help="Set the subtree pair: :") + winsync_agmt_add_parser.add_argument('--conn-timeout', help="The timeout used for replicaton connections") + winsync_agmt_add_parser.add_argument('--busy-wait-time', help="The amount of time in seconds a supplier should wait after " + "a consumer sends back a busy response before making another " + "attempt to acquire access.") + winsync_agmt_add_parser.add_argument('--session-pause-time', help="The amount of time in seconds a supplier should wait between update sessions.") + winsync_agmt_add_parser.add_argument('--init', action='store_true', default=False, help="Initialize the agreement after creating it.") + + # Set - Note can not use add's parent args because for "set" there are no "required=True" args + winsync_agmt_set_parser = winsync_agmt_subcommands.add_parser('set', help='Set an attribute in the replication winsync agreement') + winsync_agmt_set_parser.set_defaults(func=set_winsync_agmt) + winsync_agmt_set_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement') + winsync_agmt_set_parser.add_argument('--suffix', help="The DN of the replication winsync suffix") + winsync_agmt_set_parser.add_argument('--host', help="The hostname of the AD server") + winsync_agmt_set_parser.add_argument('--port', help="The port number of the AD server") + winsync_agmt_set_parser.add_argument('--conn-protocol', help="The replication winsync connection protocol: LDAP, LDAPS, or StartTLS") + winsync_agmt_set_parser.add_argument('--bind-dn', help="The Bind DN the agreement uses to authenticate to the AD Server") + winsync_agmt_set_parser.add_argument('--bind-passwd', help="The credentials for the Bind DN") + winsync_agmt_set_parser.add_argument('--frac-list', help="List of attributes to NOT replicate to the consumer during incremental updates") + winsync_agmt_set_parser.add_argument('--schedule', help="Sets the replication update schedule") + winsync_agmt_set_parser.add_argument('--win-subtree', help="The suffix of the AD Server") + winsync_agmt_set_parser.add_argument('--ds-subtree', help="The Directory Server suffix") + winsync_agmt_set_parser.add_argument('--win-domain', help="The AD Domain") + winsync_agmt_set_parser.add_argument('--sync-users', help="Synchronize Users between AD and DS") + winsync_agmt_set_parser.add_argument('--sync-groups', help="Synchronize Groups between AD and DS") + winsync_agmt_set_parser.add_argument('--sync-interval', help="The interval that DS checks AD for changes in entries") + winsync_agmt_set_parser.add_argument('--one-way-sync', help="Sets which direction to perform synchronization: \"toWindows\", \"fromWindows\", \"both\"") + winsync_agmt_set_parser.add_argument('--move-action', help="Sets instructions on how to handle moved or deleted entries: \"none\", \"unsync\", or \"delete\"") + winsync_agmt_set_parser.add_argument('--win-filter', help="Custom filter for finding users in AD Server") + winsync_agmt_set_parser.add_argument('--ds-filter', help="Custom filter for finding AD users in DS Server") + winsync_agmt_set_parser.add_argument('--subtree-pair', help="Set the subtree pair: :") + winsync_agmt_set_parser.add_argument('--conn-timeout', help="The timeout used for replicaton connections") + winsync_agmt_set_parser.add_argument('--busy-wait-time', help="The amount of time in seconds a supplier should wait after " + "a consumer sends back a busy response before making another " + "attempt to acquire access.") + winsync_agmt_set_parser.add_argument('--session-pause-time', help="The amount of time in seconds a supplier should wait between update sessions.") + + # Get + winsync_agmt_get_parser = winsync_agmt_subcommands.add_parser('get', help='Get replication configuration') + winsync_agmt_get_parser.set_defaults(func=get_winsync_agmt) + winsync_agmt_get_parser.add_argument('AGMT_NAME', nargs=1, help='Get the replication configuration for this suffix DN') + winsync_agmt_get_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix") + + ############################################ + # Replication Tasks (cleanalruv) + ############################################ + + tasks_parser = subparsers.add_parser('repl-tasks', help='Manage local (user/subtree) password policies') + task_subcommands = tasks_parser.add_subparsers(help='Replication Tasks') + + # Cleanallruv + task_cleanallruv = task_subcommands.add_parser('cleanallruv', help='Cleanup old/removed replica IDs') + task_cleanallruv.set_defaults(func=run_cleanallruv) + task_cleanallruv.add_argument('--suffix', required=True, help="The Directory Server suffix") + task_cleanallruv.add_argument('--replica-id', required=True, help="The replica ID to remove/clean") + task_cleanallruv.add_argument('--force-cleaning', action='store_true', default=False, + help="Ignore errors and do a best attempt to clean all the replicas") + + # Abort cleanallruv + task_abort_cleanallruv = task_subcommands.add_parser('abort-cleanallruv', help='Set an attribute in the replication winsync agreement') + task_abort_cleanallruv.set_defaults(func=abort_cleanallruv) + task_abort_cleanallruv.add_argument('--suffix', required=True, help="The Directory Server suffix") + task_abort_cleanallruv.add_argument('--replica-id', required=True, help="The replica ID of the cleaning task to abort") + task_abort_cleanallruv.add_argument('--certify', action='store_true', default=False, + help="Enforce that the abort task completed on all replicas") + + diff --git a/src/lib389/lib389/pwpolicy.py b/src/lib389/lib389/pwpolicy.py index 0f152b9..d665d1f 100644 --- a/src/lib389/lib389/pwpolicy.py +++ b/src/lib389/lib389/pwpolicy.py @@ -7,14 +7,11 @@ # --- END COPYRIGHT BLOCK --- import ldap -import json -from ldap import modlist from lib389._mapped_object import DSLdapObject, DSLdapObjects from lib389.config import Config -from lib389.idm.account import Account, Accounts +from lib389.idm.account import Account from lib389.idm.nscontainer import nsContainers, nsContainer from lib389.cos import CosPointerDefinitions, CosPointerDefinition, CosTemplates -from lib389.utils import ensure_str, ensure_list_str, ensure_bytes USER_POLICY = 1 SUBTREE_POLICY = 2 @@ -146,6 +143,9 @@ class PwPolicyManager(object): # Add policy to the entry user_entry.replace('pwdpolicysubentry', pwp_entry.dn) + # make sure that local policies are enabled + self.set_global_policy({'nsslapd-pwpolicy-local': 'on'}) + return pwp_entry def create_subtree_policy(self, dn, properties): @@ -187,6 +187,9 @@ class PwPolicyManager(object): 'cosTemplateDn': cos_template.dn, 'cn': 'nsPwPolicy_CoS'}) + # make sure that local policies are enabled + self.set_global_policy({'nsslapd-pwpolicy-local': 'on'}) + return pwp_entry def get_pwpolicy_entry(self, dn): diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py index cde3109..348a7b4 100644 --- a/src/lib389/lib389/replica.py +++ b/src/lib389/lib389/replica.py @@ -7,7 +7,6 @@ # --- END COPYRIGHT BLOCK --- import ldap -import os import decimal import time import logging @@ -17,8 +16,6 @@ from itertools import permutations from lib389._constants import * from lib389.properties import * from lib389.utils import normalizeDN, escapeDNValue, ensure_bytes, ensure_str, ensure_list_str, ds_is_older -from lib389._replication import RUV -from lib389.repltools import ReplTools from lib389 import DirSrv, Entry, NoSuchEntryError, InvalidArgumentError from lib389._mapped_object import DSLdapObjects, DSLdapObject from lib389.passwd import password_generate @@ -27,13 +24,10 @@ from lib389.agreement import Agreements from lib389.changelog import Changelog5 from lib389.idm.domain import Domain - from lib389.idm.group import Groups from lib389.idm.services import ServiceAccounts from lib389.idm.organizationalunit import OrganizationalUnits -from lib389.agreement import Agreements - class ReplicaLegacy(object): proxied_methods = 'search_s getEntry'.split() @@ -883,6 +877,7 @@ class RUV(object): return False return True + class Replica(DSLdapObject): """Replica DSLdapObject with: - must attributes = ['cn', 'nsDS5ReplicaType', 'nsDS5ReplicaRoot', @@ -987,29 +982,38 @@ class Replica(DSLdapObject): def _delete_agreements(self): """Delete all the agreements for the suffix - :raises: LDAPError - If failing to delete or search for agreeme :type binddn: strnts + :raises: LDAPError - If failing to delete or search for agreements """ # Get the suffix self._populate_suffix() + + # Delete standard agmts agmts = self.get_agreements() for agmt in agmts.list(): agmt.delete() - def promote(self, newrole, binddn=None, rid=None): + # Delete winysnc agmts + agmts = self.get_agreements(winsync=True) + for agmt in agmts.list(): + agmt.delete() + + def promote(self, newrole, binddn=None, binddn_group=None, rid=None): """Promote the replica to hub or master :param newrole: The new replication role for the replica: MASTER and HUB :type newrole: ReplicaRole :param binddn: The replication bind dn - only applied to master :type binddn: str + :param binddn_group: The replication bind dn group - only applied to master + :type binddn: str :param rid: The replication ID, applies only to promotions to "master" :type rid: int - :returns: None :raises: ValueError - If replica is not promoted """ - if not binddn: + + if binddn is None and binddn_group is None: binddn = defaultProperties[REPLICATION_BIND_DN] # Check the role type @@ -1025,8 +1029,14 @@ class Replica(DSLdapObject): rid = CONSUMER_REPLICAID # Create the changelog + cl = Changelog5(self._instance) try: - self._instance.changelog.create() + cl.create(properties={ + 'cn': 'changelog5', + 'nsslapd-changelogdir': self._instance.get_changelog_dir() + }) + except ldap.ALREADY_EXISTS: + pass except ldap.LDAPError as e: raise ValueError('Failed to create changelog: %s' % str(e)) @@ -1044,7 +1054,10 @@ class Replica(DSLdapObject): # Set bind dn try: - self.set(REPL_BINDDN, binddn) + if binddn: + self.set(REPL_BINDDN, binddn) + else: + self.set(REPL_BIND_GROUP, binddn_group) except ldap.LDAPError as e: raise ValueError('Failed to update replica: ' + str(e)) @@ -1169,12 +1182,13 @@ class Replica(DSLdapObject): return True - def get_agreements(self): + def get_agreements(self, winsync=False): """Return the set of agreements related to this suffix replica - + :param: winsync: If True then return winsync replication agreements, + otherwise return teh standard replication agreements. :returns: Agreements object """ - return Agreements(self._instance, self.dn) + return Agreements(self._instance, self.dn, winsync=winsync) def get_rid(self): """Return the current replicas RID for this suffix @@ -1187,6 +1201,7 @@ class Replica(DSLdapObject): """Return the in memory ruv of this replica suffix. :returns: RUV object + :raises: LDAPError """ self._populate_suffix() @@ -1201,11 +1216,29 @@ class Replica(DSLdapObject): return RUV(data) + def get_ruv_agmt_maxcsns(self): + """Return the in memory ruv of this replica suffix. + + :returns: RUV object + :raises: LDAPError + """ + self._populate_suffix() + + ent = self._instance.search_ext_s( + base=self._suffix, + scope=ldap.SCOPE_SUBTREE, + filterstr='(&(nsuniqueid=ffffffff-ffffffff-ffffffff-ffffffff)(objectclass=nstombstone))', + attrlist=['nsds5agmtmaxcsn'], + serverctrls=self._server_controls, clientctrls=self._client_controls)[0] + + return ensure_list_str(ent.getValues('nsds5agmtmaxcsn')) + def begin_task_cl2ldif(self): """Begin the changelog to ldif task """ self.replace('nsds5task', 'cl2ldif') + class Replicas(DSLdapObjects): """Replica DSLdapObjects for all replicas @@ -1239,6 +1272,7 @@ class Replicas(DSLdapObjects): replica._populate_suffix() return replica + class BootstrapReplicationManager(DSLdapObject): """A Replication Manager credential for bootstrapping the repl process. This is used by the replication manager object to coordinate the initial @@ -1255,7 +1289,8 @@ class BootstrapReplicationManager(DSLdapObject): self._must_attributes = ['cn', 'userPassword'] self._create_objectclasses = [ 'top', - 'netscapeServer' + 'netscapeServer', + 'nsAccount' ] self._protected = False self.common_name = 'replication manager'