Commit 4881826 Ticket 49926 - Add replication functionality to dsconf

12 files Authored and Committed by mreynolds 4 days ago
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!!)

    
 1 @@ -3029,8 +3029,9 @@
 2                    * temporarily mark it as "unavailable".
 3                    */
 4                   slapi_ch_free_string(&agmt->maxcsn);
 5 -                 agmt->maxcsn = slapi_ch_smprintf("%s;%s;%s;%" PRId64 ";unavailable", slapi_sdn_get_dn(agmt->replarea),
 6 -                                                  slapi_rdn_get_value_by_ref(slapi_rdn_get_rdn(agmt->rdn)), agmt->hostname, agmt->port);
 7 +                 agmt->maxcsn = slapi_ch_smprintf("%s;%s;%s;%" PRId64 ";unavailable;%s", slapi_sdn_get_dn(agmt->replarea),
 8 +                                                  slapi_rdn_get_value_by_ref(slapi_rdn_get_rdn(agmt->rdn)), agmt->hostname,
 9 +                                                  agmt->port, maxcsn);
10               } else if (rid == oprid) {
11                   slapi_ch_free_string(&agmt->maxcsn);
12                   agmt->maxcsn = slapi_ch_smprintf("%s;%s;%s;%" PRId64 ";%" PRIu16 ";%s", slapi_sdn_get_dn(agmt->replarea),
 1 @@ -26,6 +26,7 @@
 2   from lib389.cli_conf import saslmappings as cli_sasl
 3   from lib389.cli_conf import pwpolicy as cli_pwpolicy
 4   from lib389.cli_conf import backup as cli_backup
 5 + from lib389.cli_conf import replication as cli_replication
 6   from lib389.cli_conf.plugins import memberof as cli_memberof
 7   from lib389.cli_conf.plugins import usn as cli_usn
 8   from lib389.cli_conf.plugins import rootdn_ac as cli_rootdn_ac
 9 @@ -79,6 +80,7 @@
10   cli_sasl.create_parser(subparsers)
11   cli_pwpolicy.create_parser(subparsers)
12   cli_backup.create_parser(subparsers)
13 + cli_replication.create_parser(subparsers)
14   
15   argcomplete.autocomplete(parser)
16   
  1 @@ -80,6 +80,7 @@
  2       formatInfData,
  3       ensure_bytes,
  4       ensure_str,
  5 +     ensure_list_str,
  6       format_cmd_list)
  7   from lib389.paths import Paths
  8   from lib389.nss_ssl import NssSsl
  9 @@ -3286,21 +3287,28 @@
 10                                      ldif_file, e.errno, e.strerror)
 11                   raise e
 12   
 13 -     def getConsumerMaxCSN(self, replica_entry):
 14 +     def getConsumerMaxCSN(self, replica_entry, binddn=None, bindpw=None):
 15           """
 16           Attempt to get the consumer's maxcsn from its database
 17           """
 18 -         host = replica_entry.getValue(AGMT_HOST)
 19 -         port = replica_entry.getValue(AGMT_PORT)
 20 -         suffix = replica_entry.getValue(REPL_ROOT)
 21 +         host = replica_entry.get_attr_val_utf8(AGMT_HOST)
 22 +         port = replica_entry.get_attr_val_utf8(AGMT_PORT)
 23 +         suffix = replica_entry.get_attr_val_utf8(REPL_ROOT)
 24           error_msg = "Unavailable"
 25   
 26 +         # If we are using LDAPI we need to provide the credentials, otherwise
 27 +         # use the existing credentials
 28 +         if binddn is None:
 29 +             binddn = self.binddn
 30 +         if bindpw is None:
 31 +             bindpw = self.bindpw
 32 + 
 33           # Open a connection to the consumer
 34           consumer = DirSrv(verbose=self.verbose)
 35           args_instance[SER_HOST] = host
 36           args_instance[SER_PORT] = int(port)
 37 -         args_instance[SER_ROOT_DN] = self.binddn
 38 -         args_instance[SER_ROOT_PW] = self.bindpw
 39 +         args_instance[SER_ROOT_DN] = binddn
 40 +         args_instance[SER_ROOT_PW] = bindpw
 41           args_standalone = args_instance.copy()
 42           consumer.allocate(args_standalone)
 43           try:
 44 @@ -3317,7 +3325,7 @@
 45                   # Error
 46                   consumer.close()
 47                   return None
 48 -             rid = replica_entries[0].getValue(REPL_ID)
 49 +             rid = ensure_str(replica_entries[0].getValue(REPL_ID))
 50           except:
 51               # Error
 52               consumer.close()
 53 @@ -3330,8 +3338,9 @@
 54               consumer.close()
 55               if not entry:
 56                   # Error out?
 57 +                 self.log.error("Failed to retrieve database RUV entry from consumer")
 58                   return error_msg
 59 -             elements = entry[0].getValues('nsds50ruv')
 60 +             elements = ensure_list_str(entry[0].getValues('nsds50ruv'))
 61               for ruv in elements:
 62                   if ('replica %s ' % rid) in ruv:
 63                       ruv_parts = ruv.split()
 64 @@ -3345,16 +3354,17 @@
 65               consumer.close()
 66               return error_msg
 67   
 68 -     def getReplAgmtStatus(self, agmt_entry):
 69 +     def getReplAgmtStatus(self, agmt_entry, binddn=None, bindpw=None):
 70           '''
 71           Return the status message, if consumer is not in synch raise an
 72           exception
 73           '''
 74           agmt_maxcsn = None
 75 -         suffix = agmt_entry.getValue(REPL_ROOT)
 76 -         agmt_name = agmt_entry.getValue('cn')
 77 +         suffix = agmt_entry.get_attr_val_utf8(REPL_ROOT)
 78 +         agmt_name = agmt_entry.get_attr_val_utf8('cn')
 79           status = "Unknown"
 80           rc = -1
 81 + 
 82           try:
 83               entry = self.search_s(suffix, ldap.SCOPE_SUBTREE,
 84                                     REPLICA_RUV_FILTER, [AGMT_MAXCSN])
 85 @@ -3373,7 +3383,8 @@
 86               dc=example,dc=com;test_agmt;localhost;389;unavailable
 87   
 88           '''
 89 -         maxcsns = entry[0].getValues(AGMT_MAXCSN)
 90 + 
 91 +         maxcsns = ensure_list_str(entry[0].getValues(AGMT_MAXCSN))
 92           for csn in maxcsns:
 93               comps = csn.split(';')
 94               if agmt_name == comps[1]:
 95 @@ -3384,19 +3395,19 @@
 96                       agmt_maxcsn = comps[5]
 97   
 98           if agmt_maxcsn:
 99 -             con_maxcsn = self.getConsumerMaxCSN(agmt_entry)
100 +             con_maxcsn = self.getConsumerMaxCSN(agmt_entry, binddn=binddn, bindpw=bindpw)
101               if con_maxcsn:
102                   if agmt_maxcsn == con_maxcsn:
103                       status = "In Synchronization"
104                       rc = 0
105                   else:
106 -                     # Not in sync - attmpt to discover the cause
107 +                     # Not in sync - attempt to discover the cause
108                       repl_msg = "Unknown"
109 -                     if agmt_entry.getValue(AGMT_UPDATE_IN_PROGRESS) == 'TRUE':
110 +                     if agmt_entry.get_attr_val_utf8(AGMT_UPDATE_IN_PROGRESS) == 'TRUE':
111                           # Replication is on going - this is normal
112                           repl_msg = "Replication still in progress"
113                       elif "Can't Contact LDAP" in \
114 -                          agmt_entry.getValue(AGMT_UPDATE_STATUS):
115 +                          agmt_entry.get_attr_val_utf8(AGMT_UPDATE_STATUS):
116                           # Consumer is down
117                           repl_msg = "Consumer can not be contacted"
118   
 1 @@ -169,7 +169,7 @@
 2               str_attrs[ensure_str(k)] = ensure_list_str(attrs[k])
 3   
 4           # ensure all the keys are lowercase
 5 -         str_attrs = dict((k.lower(), v) for k, v in str_attrs.items())
 6 +         str_attrs = dict((k.lower(), v) for k, v in list(str_attrs.items()))
 7   
 8           response = json.dumps({"type": "entry", "dn": ensure_str(self._dn), "attrs": str_attrs})
 9   
10 @@ -969,7 +969,7 @@
11           # This may not work in all cases, especially when we consider plugins.
12           #
13           co = self._entry_to_instance(dn=None, entry=None)
14 -         # Make the rdn naming attr avaliable
15 +         # Make the rdn naming attr available
16           self._rdn_attribute = co._rdn_attribute
17           (rdn, properties) = self._validate(rdn, properties)
18           # Now actually commit the creation req
 1 @@ -83,6 +83,14 @@
 2                   retstr = "equal"
 3           return retstr
 4   
 5 +     def get_time_lag(self, oth):
 6 +         diff = oth.ts - self.ts
 7 +         if diff < 0:
 8 +             lag = datetime.timedelta(seconds=-diff)
 9 +         else:
10 +             lag = datetime.timedelta(seconds=diff)
11 +         return "{:0>8}".format(str(lag))
12 + 
13       def __repr__(self):
14           return ("%s seq: %s rid: %s" % (time.strftime("%x %X", time.localtime(self.ts)),
15                                           str(self.seq), str(self.rid)))
  1 @@ -10,13 +10,13 @@
  2   import re
  3   import time
  4   import six
  5 - 
  6 + import json
  7 + import datetime
  8   from lib389._constants import *
  9   from lib389.properties import *
 10   from lib389._entry import FormatDict
 11 - from lib389.utils import normalizeDN, ensure_bytes, ensure_str, ensure_dict_str
 12 + from lib389.utils import normalizeDN, ensure_bytes, ensure_str, ensure_dict_str, ensure_list_str
 13   from lib389 import Entry, DirSrv, NoSuchEntryError, InvalidArgumentError
 14 - 
 15   from lib389._mapped_object import DSLdapObject, DSLdapObjects
 16   
 17   
 18 @@ -33,16 +33,25 @@
 19       :type dn: str
 20       """
 21   
 22 -     def __init__(self, instance, dn=None):
 23 +     csnpat = r'(.{8})(.{4})(.{4})(.{4})'
 24 +     csnre = re.compile(csnpat)
 25 + 
 26 +     def __init__(self, instance, dn=None, winsync=False):
 27           super(Agreement, self).__init__(instance, dn)
 28           self._rdn_attribute = 'cn'
 29           self._must_attributes = [
 30               'cn',
 31           ]
 32 -         self._create_objectclasses = [
 33 -             'top',
 34 -             'nsds5replicationagreement',
 35 -         ]
 36 +         if winsync:
 37 +             self._create_objectclasses = [
 38 +                 'top',
 39 +                 'nsDSWindowsReplicationAgreement',
 40 +             ]
 41 +         else:
 42 +             self._create_objectclasses = [
 43 +                 'top',
 44 +                 'nsds5replicationagreement',
 45 +             ]
 46           self._protected = False
 47   
 48       def begin_reinit(self):
 49 @@ -59,6 +68,7 @@
 50           """
 51           done = False
 52           error = False
 53 +         inprogress = False
 54           status = self.get_attr_val_utf8('nsds5ReplicaLastInitStatus')
 55           self._log.debug('agreement tot_init status: %s' % status)
 56           if not status:
 57 @@ -67,33 +77,300 @@
 58               error = True
 59           elif 'Total update succeeded' in status:
 60               done = True
 61 +             inprogress = False
 62           elif 'Replication error' in status:
 63               error = True
 64 +         elif 'Total update in progress' in status:
 65 +             inprogress = True
 66 +         elif 'LDAP error' in status:
 67 +             error = True
 68   
 69 -         return (done, error)
 70 +         return (done, inprogress, error)
 71   
 72       def wait_reinit(self, timeout=300):
 73           """Wait for a reinit to complete. Returns done and error. A correct
 74           reinit will return (True, False).
 75 - 
 76 +         :param timeout: timeout value for how long to wait for the reinit
 77 +         :type timeout: int
 78           :returns: tuple(done, error), where done, error are bool.
 79           """
 80           done = False
 81           error = False
 82           count = 0
 83           while done is False and error is False:
 84 -             (done, error) = self.check_reinit()
 85 +             (done, inprogress, error) = self.check_reinit()
 86               if count > timeout and not done:
 87                   error = True
 88               count = count + 2
 89               time.sleep(2)
 90           return (done, error)
 91   
 92 +     def get_agmt_maxcsn(self):
 93 +         """Get the agreement maxcsn from the database RUV entry
 94 +         :returns: CSN string if found, otherwise None is returned
 95 +         """
 96 +         from lib389.replica import Replicas
 97 +         suffix = self.get_attr_val_utf8(REPL_ROOT)
 98 +         agmt_name = self.get_attr_val_utf8('cn')
 99 +         replicas = Replicas(self._instance)
100 +         replica = replicas.get(suffix)
101 +         maxcsns = replica.get_ruv_agmt_maxcsns()
102 + 
103 +         if maxcsns is None or len(maxcsns) == 0:
104 +             self._log.debug('get_agmt_maxcsn - Failed to get agmt maxcsn from RUV')
105 +             return None
106 + 
107 +         for csn in maxcsns:
108 +             comps = csn.split(';')
109 +             if agmt_name == comps[1]:
110 +                 # same replica, get maxcsn
111 +                 if len(comps) < 6:
112 +                     return None
113 +                 else:
114 +                     return comps[5]
115 + 
116 +         self._log.debug('get_agmt_maxcsn - did not find matching agmt maxcsn from RUV')
117 +         return None
118 + 
119 +     def get_consumer_maxcsn(self, binddn=None, bindpw=None):
120 +         """Attempt to get the consumer's maxcsn from its database RUV entry
121 +         :param binddn: Specifies a specific bind DN to use when contacting the remote consumer
122 +         :type binddn: str
123 +         :param bindpw: Password for the bind DN
124 +         :type bindpw: str
125 +         :returns: CSN string if found, otherwise "Unavailable" is returned
126 +         """
127 +         host = self.get_attr_val_utf8(AGMT_HOST)
128 +         port = self.get_attr_val_utf8(AGMT_PORT)
129 +         suffix = self.get_attr_val_utf8(REPL_ROOT)
130 +         protocol = self.get_attr_val_utf8('nsds5replicatransportinfo').lower()
131 + 
132 +         result_msg = "Unavailable"
133 + 
134 +         # If we are using LDAPI we need to provide the credentials, otherwise
135 +         # use the existing credentials
136 +         if binddn is None:
137 +             binddn = self._instance.binddn
138 +         if bindpw is None:
139 +             bindpw = self._instance.bindpw
140 + 
141 +         # Get the replica id from supplier to compare to the consumer's rid
142 +         from lib389.replica import Replicas
143 +         replicas = Replicas(self._instance)
144 +         replica = replicas.get(suffix)
145 +         rid = replica.get_attr_val_utf8(REPL_ID)
146 + 
147 +         # Open a connection to the consumer
148 +         consumer = DirSrv(verbose=self._instance.verbose)
149 +         args_instance[SER_HOST] = host
150 +         if protocol == "ssl" or protocol == "ldaps":
151 +             args_instance[SER_SECURE_PORT] = int(port)
152 +         else:
153 +             args_instance[SER_PORT] = int(port)
154 +         args_instance[SER_ROOT_DN] = binddn
155 +         args_instance[SER_ROOT_PW] = bindpw
156 +         args_standalone = args_instance.copy()
157 +         consumer.allocate(args_standalone)
158 +         try:
159 +             consumer.open()
160 +         except ldap.LDAPError as e:
161 +             self._instance.log.debug('Connection to consumer ({}:{}) failed, error: {}'.format(host, port, e))
162 +             return result_msg
163 + 
164 +         # Search for the tombstone RUV entry
165 +         try:
166 +             entry = consumer.search_s(suffix, ldap.SCOPE_SUBTREE,
167 +                                       REPLICA_RUV_FILTER, ['nsds50ruv'])
168 +             if not entry:
169 +                 self.log.error("Failed to retrieve database RUV entry from consumer")
170 +             else:
171 +                 elements = ensure_list_str(entry[0].getValues('nsds50ruv'))
172 +                 for ruv in elements:
173 +                     if ('replica %s ' % rid) in ruv:
174 +                         ruv_parts = ruv.split()
175 +                         if len(ruv_parts) == 5:
176 +                             result_msg = ruv_parts[4]
177 +                         break
178 +         except ldap.LDAPError as e:
179 +             self._instance.log.debug('Failed to search for the suffix ' +
180 +                                      '({}) consumer ({}:{}) failed, error: {}'.format(
181 +                                          suffix, host, port, e))
182 +         consumer.close()
183 +         return result_msg
184 + 
185 +     def get_agmt_status(self, binddn=None, bindpw=None):
186 +         """Return the status message
187 +         :param binddn: Specifies a specific bind DN to use when contacting the remote consumer
188 +         :type binddn: str
189 +         :param bindpw: Password for the bind DN
190 +         :type bindpw: str
191 +         :returns: A status message about the replication agreement
192 +         """
193 +         status = "Unknown"
194 + 
195 +         agmt_maxcsn = self.get_agmt_maxcsn()
196 +         if agmt_maxcsn is not None:
197 +             con_maxcsn = self.get_consumer_maxcsn(binddn=binddn, bindpw=bindpw)
198 +             if con_maxcsn:
199 +                 if agmt_maxcsn == con_maxcsn:
200 +                     status = "In Synchronization"
201 +                 else:
202 +                     # Not in sync - attempt to discover the cause
203 +                     repl_msg = "Unknown"
204 +                     if self.get_attr_val_utf8(AGMT_UPDATE_IN_PROGRESS) == 'TRUE':
205 +                         # Replication is on going - this is normal
206 +                         repl_msg = "Replication still in progress"
207 +                     elif "Can't Contact LDAP" in \
208 +                          self.get_attr_val_utf8(AGMT_UPDATE_STATUS):
209 +                         # Consumer is down
210 +                         repl_msg = "Consumer can not be contacted"
211 + 
212 +                     status = ("Not in Synchronization: supplier " +
213 +                               "(%s) consumer (%s)  Reason(%s)" %
214 +                               (agmt_maxcsn, con_maxcsn, repl_msg))
215 +         return status
216 + 
217 +     def get_lag_time(self, suffix, agmt_name, binddn=None, bindpw=None):
218 +         """Get the lag time between the supplier and the consumer
219 +         :param suffix: The replication suffix
220 +         :type suffix: str
221 +         :param agmt_name: The name of the agreement
222 +         :type agmt_name: str
223 +         :param binddn: Specifies a specific bind DN to use when contacting the remote consumer
224 +         :type binddn: str
225 +         :param bindpw: Password for the bind DN
226 +         :type bindpw: str
227 +         :returns: A time-formated string of the the replication lag (HH:MM:SS).
228 +         :raises: ValueError - if unable to get consumer's maxcsn
229 +         """
230 +         agmt_maxcsn = self.get_agmt_maxcsn()
231 +         con_maxcsn = self.get_consumer_maxcsn(binddn=binddn, bindpw=bindpw)
232 +         if con_maxcsn is None:
233 +             raise ValueError("Unable to get consumer's max csn")
234 +         if con_maxcsn == "Unavailable":
235 +             return con_maxcsn
236 + 
237 +         # Extract the csn timstamps and compare them
238 +         match = Agreement.csnre.match(agmt_maxcsn)
239 +         if match:
240 +             agmt_time = int(match.group(1), 16)
241 +         match = Agreement.csnre.match(con_maxcsn)
242 +         if match:
243 +             con_time = int(match.group(1), 16)
244 +         diff = con_time - agmt_time
245 +         if diff < 0:
246 +             lag = datetime.timedelta(seconds=-diff)
247 +         else:
248 +             lag = datetime.timedelta(seconds=diff)
249 + 
250 +         # Return a nice formated timestamp
251 +         return "{:0>8}".format(str(lag))
252 + 
253 +     def status(self, winsync=False, just_status=False, use_json=False, binddn=None, bindpw=None):
254 +         """Get the status of a replication agreement
255 +         :param winsync: Specifies if the the agreement is a winsync replication agreement
256 +         :type winsync: boolean
257 +         :param just_status: Just return the status string and not all of the status attributes
258 +         :type just_status: boolean
259 +         :param use_json: Return the status in a JSON object
260 +         :type use_json: boolean
261 +         :param binddn: Specifies a specific bind DN to use when contacting the remote consumer
262 +         :type binddn: str
263 +         :param bindpw: Password for the bind DN
264 +         :type bindpw: str
265 +         :returns: A status message
266 +         :raises: ValueError - if failing to get agmt status
267 +         """
268 +         status_attrs_dict = self.get_all_attrs()
269 +         status_attrs_dict = dict((k.lower(), v) for k, v in list(status_attrs_dict.items()))
270 + 
271 +         # We need a bind DN and passwd so we can query the consumer.  If this is an LDAPI
272 +         # connection, and the consumer does not allow anonymous access to the tombstone
273 +         # RUV entry under the suffix, then we can't get the status.  So in this case we
274 +         # need to provide a DN and password.
275 +         if not winsync:
276 +             try:
277 +                 status = self.get_agmt_status(binddn=binddn, bindpw=bindpw)
278 +             except ValueError as e:
279 +                 status = str(e)
280 +             if just_status:
281 +                 if use_json:
282 +                     return (json.dumps(status))
283 +                 else:
284 +                     return status
285 + 
286 +             # Get the lag time
287 +             suffix = ensure_str(status_attrs_dict['nsds5replicaroot'][0])
288 +             agmt_name = ensure_str(status_attrs_dict['cn'][0])
289 +             lag_time = self.get_lag_time(suffix, agmt_name, binddn=binddn, bindpw=bindpw)
290 +         else:
291 +             status = "Not available for Winsync agreements"
292 + 
293 +         # handle the attributes that are not always set in the agreement
294 +         if 'nsds5replicaenabled' not in status_attrs_dict:
295 +             status_attrs_dict['nsds5replicaenabled'] = ['on']
296 +         if 'nsds5agmtmaxcsn' not in status_attrs_dict:
297 +             status_attrs_dict['nsds5agmtmaxcsn'] = ["unavailable"]
298 +         if 'nsds5replicachangesskippedsince' not in status_attrs_dict:
299 +             status_attrs_dict['nsds5replicachangesskippedsince'] = ["unavailable"]
300 +         if 'nsds5beginreplicarefresh' not in status_attrs_dict:
301 +             status_attrs_dict['nsds5beginreplicarefresh'] = [""]
302 +         if 'nsds5replicalastinitstatus' not in status_attrs_dict:
303 +             status_attrs_dict['nsds5replicalastinitstatus'] = ["unavilable"]
304 +         if 'nsds5replicachangessentsincestartup' not in status_attrs_dict:
305 +             status_attrs_dict['nsds5replicachangessentsincestartup'] = ['0']
306 +         if ensure_str(status_attrs_dict['nsds5replicachangessentsincestartup'][0]) == '':
307 +             status_attrs_dict['nsds5replicachangessentsincestartup'] = ['0']
308 + 
309 +         # Case sensitive?
310 +         if use_json:
311 +             result = {'replica-enabled': ensure_str(status_attrs_dict['nsds5replicaenabled'][0]),
312 +                       'update-in-progress': ensure_str(status_attrs_dict['nsds5replicaupdateinprogress'][0]),
313 +                       'last-update-start': ensure_str(status_attrs_dict['nsds5replicalastupdatestart'][0]),
314 +                       'last-update-end': ensure_str(status_attrs_dict['nsds5replicalastupdateend'][0]),
315 +                       'number-changes-sent': ensure_str(status_attrs_dict['nsds5replicachangessentsincestartup'][0]),
316 +                       'number-changes-skipped:': ensure_str(status_attrs_dict['nsds5replicachangesskippedsince'][0]),
317 +                       'last-update-status': ensure_str(status_attrs_dict['nsds5replicalastupdatestatus'][0]),
318 +                       'init-in-progress': ensure_str(status_attrs_dict['nsds5beginreplicarefresh'][0]),
319 +                       'last-init-start': ensure_str(status_attrs_dict['nsds5replicalastinitstart'][0]),
320 +                       'last-init-end': ensure_str(status_attrs_dict['nsds5replicalastinitend'][0]),
321 +                       'last-init-status': ensure_str(status_attrs_dict['nsds5replicalastinitstatus'][0]),
322 +                       'reap-active': ensure_str(status_attrs_dict['nsds5replicareapactive'][0]),
323 +                       'replication-status': status,
324 +                       'replication-lag-time': lag_time
325 +                 }
326 +             return (json.dumps(result))
327 +         else:
328 +             retstr = (
329 +                 "Status for %(cn)s agmt %(nsDS5ReplicaHost)s:"
330 +                 "%(nsDS5ReplicaPort)s" "\n"
331 +                 "Replica Enabled: %(nsds5ReplicaEnabled)s" "\n"
332 +                 "Update In Progress: %(nsds5replicaUpdateInProgress)s" "\n"
333 +                 "Last Update Start: %(nsds5replicaLastUpdateStart)s" "\n"
334 +                 "Last Update End: %(nsds5replicaLastUpdateEnd)s" "\n"
335 +                 "Number Of Changes Sent: %(nsds5replicaChangesSentSinceStartup)s"
336 +                 "\n"
337 +                 "Number Of Changes Skipped: %(nsds5replicaChangesSkippedSince"
338 +                 "Startup)s" "\n"
339 +                 "Last Update Status: %(nsds5replicaLastUpdateStatus)s" "\n"
340 +                 "Init In Progress: %(nsds5BeginReplicaRefresh)s" "\n"
341 +                 "Last Init Start: %(nsds5ReplicaLastInitStart)s" "\n"
342 +                 "Last Init End: %(nsds5ReplicaLastInitEnd)s" "\n"
343 +                 "Last Init Status: %(nsds5ReplicaLastInitStatus)s" "\n"
344 +                 "Reap Active: %(nsds5ReplicaReapActive)s" "\n"
345 +             )
346 +             # FormatDict manages missing fields in string formatting
347 +             entry_data = ensure_dict_str(status_attrs_dict)
348 +             result = retstr % FormatDict(entry_data)
349 +             result += "Replication Status: {}\n".format(status)
350 +             result += "Replication Lag Time: {}\n".format(lag_time)
351 +             return result
352 + 
353       def pause(self):
354           """Pause outgoing changes from this server to consumer. Note
355           that this does not pause the consumer, only that changes will
356           not be sent from this master to consumer: the consumer may still
357 -         recieve changes from other replication paths!
358 +         receive changes from other replication paths!
359           """
360           self.set('nsds5ReplicaEnabled', 'off')
361   
362 @@ -122,6 +399,34 @@
363           """
364           return self.get_attr_val_utf8('nsDS5ReplicaWaitForAsyncResults')
365   
366 + 
367 + class WinsyncAgreement(Agreement):
368 +     """A replication agreement from this server instance to
369 +     another instance of directory server.
370 + 
371 +     - must attributes: [ 'cn' ]
372 +     - RDN attribute: 'cn'
373 + 
374 +     :param instance: An instance
375 +     :type instance: lib389.DirSrv
376 +     :param dn: Entry DN
377 +     :type dn: str
378 +     """
379 + 
380 +     def __init__(self, instance, dn=None):
381 +         super(Agreement, self).__init__(instance, dn)
382 +         self._rdn_attribute = 'cn'
383 +         self._must_attributes = [
384 +             'cn',
385 +         ]
386 +         self._create_objectclasses = [
387 +                 'top',
388 +                 'nsDSWindowsReplicationAgreement',
389 +             ]
390 + 
391 +         self._protected = False
392 + 
393 + 
394   class Agreements(DSLdapObjects):
395       """Represents the set of agreements configured on this instance.
396       There are two possible ways to use this interface.
397 @@ -149,11 +454,15 @@
398       :type rdn: str
399       """
400   
401 -     def __init__(self, instance, basedn=DN_MAPPING_TREE, rdn=None):
402 +     def __init__(self, instance, basedn=DN_MAPPING_TREE, rdn=None, winsync=False):
403           super(Agreements, self).__init__(instance)
404 -         self._childobject = Agreement
405 -         self._objectclasses = [ 'nsds5replicationagreement' ]
406 -         self._filterattrs = [ 'cn', 'nsDS5ReplicaRoot' ]
407 +         if winsync:
408 +             self._childobject = WinsyncAgreement
409 +             self._objectclasses = ['nsDSWindowsReplicationAgreement']
410 +         else:
411 +             self._childobject = Agreement
412 +             self._objectclasses = ['nsds5replicationagreement']
413 +         self._filterattrs = ['cn', 'nsDS5ReplicaRoot']
414           if rdn is None:
415               self._basedn = basedn
416           else:
417 @@ -167,6 +476,7 @@
418               raise ldap.UNWILLING_TO_PERFORM("Refusing to create agreement in %s" % DN_MAPPING_TREE)
419           return super(Agreements, self)._validate(rdn, properties)
420   
421 + 
422   class AgreementLegacy(object):
423       """An object that helps to work with agreement entry
424   
425 @@ -194,7 +504,6 @@
426           :type agreement_dn: str
427           :param just_status: If True, returns just status
428           :type just_status: bool
429 - 
430           :returns: str -- See below
431           :raises: NoSuchEntryError - if agreement_dn is an unknown entry
432   
433 @@ -208,7 +517,7 @@
434                   Last Update End: 0
435                   Num. Changes Sent: 1:10/0
436                   Num. changes Skipped: None
437 -                 Last update Status: 0 Replica acquired successfully:
438 +                 Last update Status: Error (0) Replica acquired successfully:
439                       Incremental update started
440                   Init in progress: None
441                   Last Init Start: 0
 1 @@ -16,6 +16,7 @@
 2   from lib389._mapped_object import DSLdapObject
 3   from lib389.utils import ds_is_older
 4   
 5 + 
 6   class Changelog5(DSLdapObject):
 7       """Represents the Directory Server changelog. This is used for
 8       replication. Only one changelog is needed for every server.
 9 @@ -25,9 +26,9 @@
10       """
11   
12       def __init__(self, instance, dn='cn=changelog5,cn=config'):
13 -         super(Changelog5,self).__init__(instance, dn)
14 +         super(Changelog5, self).__init__(instance, dn)
15           self._rdn_attribute = 'cn'
16 -         self._must_attributes = [ 'cn', 'nsslapd-changelogdir' ]
17 +         self._must_attributes = ['cn', 'nsslapd-changelogdir']
18           self._create_objectclasses = [
19               'top',
20               'nsChangelogConfig',
21 @@ -37,7 +38,7 @@
22                   'top',
23                   'extensibleobject',
24               ]
25 -         self._protected = True
26 +         self._protected = False
27   
28       def set_max_entries(self, value):
29           """Configure the max entries the changelog can hold.
 1 @@ -163,12 +163,12 @@
 2                                  help="Specifies the filename of the input LDIF files."
 3                                       "When multiple files are imported, they are imported in the order"
 4                                       "they are specified on the command line.")
 5 -     import_parser.add_argument('-c', '--chunks_size', type=int,
 6 +     import_parser.add_argument('-c', '--chunks-size', type=int,
 7                                  help="The number of chunks to have during the import operation.")
 8       import_parser.add_argument('-E', '--encrypted', action='store_true',
 9                                  help="Decrypts encrypted data during export. This option is used only"
10                                       "if database encryption is enabled.")
11 -     import_parser.add_argument('-g', '--gen_uniq_id',
12 +     import_parser.add_argument('-g', '--gen-uniq-id',
13                                  help="Generate a unique id. Type none for no unique ID to be generated"
14                                       "and deterministic for the generated unique ID to be name-based."
15                                       "By default, a time-based unique ID is generated."
16 @@ -176,11 +176,11 @@
17                                       "it is also possible to specify the namespace for the server to use."
18                                       "namespaceId is a string of characters"
19                                       "in the format 00-xxxxxxxx-xxxxxxxx-xxxxxxxx-xxxxxxxx.")
20 -     import_parser.add_argument('-O', '--only_core', action='store_true',
21 +     import_parser.add_argument('-O', '--only-core', action='store_true',
22                                  help="Requests that only the core database is created without attribute indexes.")
23 -     import_parser.add_argument('-s', '--include_suffixes', nargs='+',
24 +     import_parser.add_argument('-s', '--include-suffixes', nargs='+',
25                                  help="Specifies the suffixes or the subtrees to be included.")
26 -     import_parser.add_argument('-x', '--exclude_suffixes', nargs='+',
27 +     import_parser.add_argument('-x', '--exclude-suffixes', nargs='+',
28                                  help="Specifies the suffixes to be excluded.")
29   
30       export_parser = subcommands.add_parser('export', help='do an online export of the suffix')
31 @@ -190,21 +190,21 @@
32       export_parser.add_argument('-l', '--ldif',
33                                  help="Gives the filename of the output LDIF file."
34                                       "If more than one are specified, use a space as a separator")
35 -     export_parser.add_argument('-C', '--use_id2entry', action='store_true', help="Uses only the main database file.")
36 +     export_parser.add_argument('-C', '--use-id2entry', action='store_true', help="Uses only the main database file.")
37       export_parser.add_argument('-E', '--encrypted', action='store_true',
38                                  help="""Decrypts encrypted data during export. This option is used only
39                                          if database encryption is enabled.""")
40 -     export_parser.add_argument('-m', '--min_base64', action='store_true',
41 +     export_parser.add_argument('-m', '--min-base64', action='store_true',
42                                  help="Sets minimal base-64 encoding.")
43 -     export_parser.add_argument('-N', '--no_seq_num', action='store_true',
44 +     export_parser.add_argument('-N', '--no-seq-num', action='store_true',
45                                  help="Enables you to suppress printing the sequence number.")
46       export_parser.add_argument('-r', '--replication', action='store_true',
47                                  help="Exports the information required to initialize a replica when the LDIF is imported")
48 -     export_parser.add_argument('-u', '--no_dump_uniq_id', action='store_true',
49 +     export_parser.add_argument('-u', '--no-dump-uniq-id', action='store_true',
50                                  help="Requests that the unique ID is not exported.")
51 -     export_parser.add_argument('-U', '--not_folded', action='store_true',
52 +     export_parser.add_argument('-U', '--not-folded', action='store_true',
53                                  help="Requests that the output LDIF is not folded.")
54 -     export_parser.add_argument('-s', '--include_suffixes', nargs='+',
55 +     export_parser.add_argument('-s', '--include-suffixes', nargs='+',
56                                  help="Specifies the suffixes or the subtrees to be included.")
57 -     export_parser.add_argument('-x', '--exclude_suffixes', nargs='+',
58 +     export_parser.add_argument('-x', '--exclude-suffixes', nargs='+',
59                                  help="Specifies the suffixes to be excluded.")
1 @@ -147,7 +147,7 @@
2                   result += "%s (%s)\n" % (entrydn, policy_type.lower())
3   
4       if args.json:
5 -         return print(json.dumps(result))
6 +         print(json.dumps(result))
7       else:
8           log.info(result)
9   
   1 @@ -0,0 +1,1046 @@
   2 + # --- BEGIN COPYRIGHT BLOCK ---
   3 + # Copyright (C) 2018 Red Hat, Inc.
   4 + # All rights reserved.
   5 + #
   6 + # License: GPL (version 3 or any later version).
   7 + # See LICENSE for details.
   8 + # --- END COPYRIGHT BLOCK ---
   9 + 
  10 + import json
  11 + import ldap
  12 + from getpass import getpass
  13 + from lib389._constants import *
  14 + from lib389.changelog import Changelog5
  15 + from lib389.utils import is_a_dn
  16 + from lib389.replica import Replicas, BootstrapReplicationManager
  17 + from lib389.tasks import CleanAllRUVTask, AbortCleanAllRUVTask
  18 + 
  19 + 
  20 + arg_to_attr = {
  21 +         # replica config
  22 +         'replica_id': 'nsds5replicaid',
  23 +         'repl_purge_delay': 'nsds5replicapurgedelay',
  24 +         'repl_tombstone_purge_interval': 'nsds5replicatombstonepurgeinterval',
  25 +         'repl_fast_tombstone_purging': 'nsds5ReplicaPreciseTombstonePurging',
  26 +         'repl_bind_group': 'nsds5replicabinddngroup',
  27 +         'repl_bind_group_interval': 'nsds5replicabinddngroupcheckinterval',
  28 +         'repl_protocol_timeout': 'nsds5replicaprotocoltimeout',
  29 +         'repl_backoff_min': 'nsds5replicabackoffmin',
  30 +         'repl_backoff_max': 'nsds5replicabackoffmax',
  31 +         'repl_release_timeout': 'nsds5replicareleasetimeout',
  32 +         # Changelog
  33 +         'max_entries': 'nsslapd-changelogmaxentries',
  34 +         'max_age': 'nsslapd-changelogmaxage',
  35 +         'compact_interval': 'nsslapd-changelogcompactdb-interval',
  36 +         'trim_interval': 'nsslapd-changelogtrim-interval',
  37 +         'encrypt_algo': 'nsslapd-encryptionalgorithm',
  38 +         'encrypt_key': 'nssymmetrickey',
  39 +         # Agreement
  40 +         'host': 'nsds5replicahost',
  41 +         'port': 'nsds5replicaport',
  42 +         'conn_protocol': 'nsds5replicatransportinfo',
  43 +         'bind_dn': 'nsds5replicabinddn',
  44 +         'bind_passwd': 'nsds5replicacredentials',
  45 +         'bind_method': 'nsds5replicabindmethod',
  46 +         'frac_list': 'nsds5replicatedattributelist',
  47 +         'frac_list_total': 'nsds5replicatedattributelisttotal',
  48 +         'strip_list': 'nsds5replicastripattrs',
  49 +         'schedule': 'nsds5replicaupdateschedule',
  50 +         'conn_timeout': 'nsds5replicatimeout',
  51 +         'protocol_timeout': 'nsds5replicaprotocoltimeout',
  52 +         'wait_async_results': 'nsds5replicawaitforasyncresults',
  53 +         'busy_wait_time': 'nsds5replicabusywaittime',
  54 +         'session_pause_time': 'nsds5replicaSessionPauseTime',
  55 +         'flow_control_window': 'nsds5replicaflowcontrolwindow',
  56 +         'flow_control_pause': 'nsds5replicaflowcontrolpause',
  57 +         # Additional Winsync Agmt attrs
  58 +         'win_subtree': 'nsds7windowsreplicasubtree',
  59 +         'ds_subtree': 'nsds7directoryreplicasubtree',
  60 +         'sync_users': 'nsds7newwinusersyncenabled',
  61 +         'sync_groups': 'nsds7newwingroupsyncenabled',
  62 +         'win_domain': 'nsds7windowsDomain',
  63 +         'sync_interval': 'winsyncinterval',
  64 +         'one_way_sync': 'onewaysync',
  65 +         'move_action': 'winsyncmoveAction',
  66 +         'ds_filter': 'winsyncdirectoryfilter',
  67 +         'win_filter': 'winsyncwindowsfilter',
  68 +         'subtree_pair': 'winSyncSubtreePair'
  69 +     }
  70 + 
  71 + 
  72 + def get_agmt(inst, args, winsync=False):
  73 +     agmt_name = args.AGMT_NAME[0]
  74 +     replicas = Replicas(inst)
  75 +     replica = replicas.get(args.suffix)
  76 +     agmts = replica.get_agreements(winsync=winsync)
  77 +     try:
  78 +         agmt = agmts.get(agmt_name)
  79 +     except ldap.NO_SUCH_OBJECT:
  80 +         raise ValueError("Could not find the agreement \"{}\" for suffix \"{}\"".format(agmt_name, args.suffix))
  81 +     return agmt
  82 + 
  83 + 
  84 + def _args_to_attrs(args):
  85 +     attrs = {}
  86 +     for arg in vars(args):
  87 +         val = getattr(args, arg)
  88 +         if arg in arg_to_attr and val is not None:
  89 +             attrs[arg_to_attr[arg]] = val
  90 +     return attrs
  91 + 
  92 + 
  93 + #
  94 + # Replica config
  95 + #
  96 + def enable_replication(inst, basedn, log, args):
  97 +     repl_root = args.suffix
  98 +     role = args.role.lower()
  99 +     rid = args.replica_id
 100 + 
 101 +     if role == "master":
 102 +         repl_type = '3'
 103 +         repl_flag = '1'
 104 +     elif role == "hub":
 105 +         repl_type = '2'
 106 +         repl_flag = '1'
 107 +     elif role == "consumer":
 108 +         repl_type = '2'
 109 +         repl_flag = '0'
 110 +     else:
 111 +         # error - unknown type
 112 +         raise ValueError("Unknown replication role ({}), you must use \"master\", \"hub\", or \"consumer\"".format(role))
 113 + 
 114 +     # Start the propeties and update them as needed
 115 +     repl_properties = {
 116 +         'cn': 'replica',
 117 +         'nsDS5ReplicaRoot': repl_root,
 118 +         'nsDS5Flags': repl_flag,
 119 +         'nsDS5ReplicaType': repl_type,
 120 +         }
 121 + 
 122 +     # Validate master settings
 123 +     if role == "master":
 124 +         # Do we have a rid?
 125 +         if not args.replica_id or args.replica_id is None:
 126 +             # Error, master needs a rid TODO
 127 +             raise ValueError('You must specify the replica ID (--replica-id) when enabling a \"master\" replica')
 128 + 
 129 +         # is it a number?
 130 +         try:
 131 +             rid_num = int(rid)
 132 +         except ValueError:
 133 +             raise ValueError("--rid expects a number between 1 and 65534")
 134 + 
 135 +         # Is it in range?
 136 +         if rid_num < 1 or rid_num > 65534:
 137 +             raise ValueError("--replica-id expects a number between 1 and 65534")
 138 + 
 139 +         # rid is good add it to the props
 140 +         repl_properties['nsDS5ReplicaId'] = rid
 141 + 
 142 +     # Bind DN or Bind DN Group?
 143 +     if args.bind_group_dn:
 144 +         repl_properties['nsDS5ReplicaBindDNGroup'] = args.bind_group_dn
 145 +     elif args.bind_dn:
 146 +         repl_properties['nsDS5ReplicaBindDN'] = args.bind_dn
 147 + 
 148 +     # First create the changelog
 149 +     cl = Changelog5(inst)
 150 +     try:
 151 +         cl.create(properties={
 152 +             'cn': 'changelog5',
 153 +             'nsslapd-changelogdir': inst.get_changelog_dir()
 154 +         })
 155 +     except ldap.ALREADY_EXISTS:
 156 +         pass
 157 + 
 158 +     # Finally enable replication
 159 +     replicas = Replicas(inst)
 160 +     try:
 161 +         replicas.create(properties=repl_properties)
 162 +     except ldap.ALREADY_EXISTS:
 163 +         raise ValueError("Replication is already enabled for this suffix")
 164 + 
 165 +     print("Replication successfully enabled for \"{}\"".format(repl_root))
 166 + 
 167 + 
 168 + def disable_replication(inst, basedn, log, args):
 169 +     replicas = Replicas(inst)
 170 +     try:
 171 +         replica = replicas.get(args.suffix)
 172 +         replica.delete()
 173 +     except ldap.NO_SUCH_OBJECT:
 174 +         raise ValueError("Backend \"{}\" is not enabled for replication".format(args.suffix))
 175 +     print("Replication disabled for \"{}\"".format(args.suffix))
 176 + 
 177 + 
 178 + def promote_replica(inst, basedn, log, args):
 179 +     replicas = Replicas(inst)
 180 +     replica = replicas.get(args.suffix)
 181 +     role = args.newrole.lower()
 182 + 
 183 +     if role == 'master':
 184 +         newrole = ReplicaRole.MASTER
 185 +         if args.rreplica_idid is None:
 186 +             raise ValueError("You need to provide a replica ID (--replica-id) to promote replica to a master")
 187 +     elif role == 'hub':
 188 +         newrole = ReplicaRole.HUB
 189 +     else:
 190 +         raise ValueError("Invalid role ({}), you must use either \"master\" or \"hub\"".format(role))
 191 + 
 192 +     replica.promote(newrole, binddn=args.bind_dn, binddn_group=args.bind_group_dn, rid=args.replica_id)
 193 +     print("Successfully promoted replica to \"{}\"".format(role))
 194 + 
 195 + 
 196 + def demote_replica(inst, basedn, log, args):
 197 +     replicas = Replicas(inst)
 198 +     replica = replicas.get(args.suffix)
 199 +     role = args.newrole.lower()
 200 + 
 201 +     if role == 'hub':
 202 +         newrole = ReplicaRole.HUB
 203 +     elif role == 'consumer':
 204 +         newrole = ReplicaRole.CONSUMER
 205 +     else:
 206 +         raise ValueError("Invalid role ({}), you must use either \"hub\" or \"consumer\"".format(role))
 207 + 
 208 +     replica.demote(newrole)
 209 +     print("Successfully demoted replica to \"{}\"".format(role))
 210 + 
 211 + 
 212 + def get_repl_config(inst, basedn, log, args):
 213 +     replicas = Replicas(inst)
 214 +     replica = replicas.get(args.suffix)
 215 +     if args and args.json:
 216 +         print(replica.get_all_attrs_json())
 217 +     else:
 218 +         log.info(replica.display())
 219 + 
 220 + 
 221 + def set_repl_config(inst, basedn, log, args):
 222 +     replicas = Replicas(inst)
 223 +     replica = replicas.get(args.suffix)
 224 +     attrs = _args_to_attrs(args)
 225 +     op_count = 0
 226 + 
 227 +     # Add supplier DNs
 228 +     if args.repl_add_bind_dn is not None:
 229 +         if not is_a_dn(repl_add_bind_dn):
 230 +             raise ValueError("The replica bind DN is not a valid DN")
 231 +         replica.add('nsds5ReplicaBindDN', args.repl_add_bind_dn)
 232 +         op_count += 1
 233 + 
 234 +     # Remove supplier DNs
 235 +     if args.repl_del_bind_dn is not None:
 236 +         replica.remove('nsds5ReplicaBindDN', args.repl_del_bind_dn)
 237 +         op_count += 1
 238 + 
 239 +     # Add referral
 240 +     if args.repl_add_ref is not None:
 241 +         replica.add('nsDS5ReplicaReferral', args.repl_add_ref)
 242 +         op_count += 1
 243 + 
 244 +     # Remove referral
 245 +     if args.repl_del_ref is not None:
 246 +         replica.remove('nsDS5ReplicaReferral', args.repl_del_ref)
 247 +         op_count += 1
 248 + 
 249 +     # Handle the rest of the changes that use mod_replace
 250 +     modlist = []
 251 +     for attr, value in attrs.items():
 252 +         modlist.append((attr, value))
 253 +     if len(modlist) > 0:
 254 +         replica.replace_many(*modlist)
 255 +     elif op_count == 0:
 256 +         raise ValueError("There are no changes to set in the replica")
 257 +     print("Successfully updated replication configuration")
 258 + 
 259 + 
 260 + def create_cl(inst, basedn, log, args):
 261 +     cl = Changelog5(inst)
 262 +     try:
 263 +         cl.create(properties={
 264 +             'cn': 'changelog5',
 265 +             'nsslapd-changelogdir': inst.get_changelog_dir()
 266 +         })
 267 +     except ldap.ALREADY_EXISTS:
 268 +         raise ValueError("Changelog already exists")
 269 +     print("Successfully created replication changelog")
 270 + 
 271 + 
 272 + def delete_cl(inst, basedn, log, args):
 273 +     cl = Changelog5(inst)
 274 +     try:
 275 +         cl.delete()
 276 +     except ldap.NO_SUCH_OBJECT:
 277 +         raise ValueError("There is no changelog to delete")
 278 +     print("Successfully deleted replication changelog")
 279 + 
 280 + 
 281 + def set_cl(inst, basedn, log, args):
 282 +     cl = Changelog5(inst)
 283 +     attrs = _args_to_attrs(args)
 284 +     modlist = []
 285 +     for attr, value in attrs.items():
 286 +         modlist.append((attr, value))
 287 +     if len(modlist) > 0:
 288 +         cl.replace_many(*modlist)
 289 +     else:
 290 +         raise ValueError("There are no changes to set for the replication changelog")
 291 +     print("Successfully updated replication changelog")
 292 + 
 293 + 
 294 + def get_cl(inst, basedn, log, args):
 295 +     cl = Changelog5(inst)
 296 +     if args and args.json:
 297 +         print(cl.get_all_attrs_json())
 298 +     else:
 299 +         log.info(cl.display())
 300 + 
 301 + 
 302 + def create_repl_manager(inst, basedn, log, args):
 303 +     manager_cn = "replication manager"
 304 +     repl_manager_password = ""
 305 +     repl_manager_password_confirm = ""
 306 + 
 307 +     if args.name:
 308 +         manager_cn = args.name
 309 + 
 310 +     if is_a_dn(manager_cn):
 311 +         # A full DN was provided, make sure it uses "cn" for the RDN
 312 +         if manager_cn.split("=", 1)[0].lower() != "cn":
 313 +             raise ValueError("Replication manager DN must use \"cn\" for the rdn attribute")
 314 +         manager_dn = manager_cn
 315 +     else:
 316 +         manager_dn = "cn={},cn=config".format(manager_cn)
 317 + 
 318 +     if args.passwd:
 319 +         repl_manager_password = args.passwd
 320 +     else:
 321 +         # Prompt for password
 322 +         while 1:
 323 +             while repl_manager_password == "":
 324 +                 repl_manager_password = getpass("Enter replication manager password: ")
 325 +             while repl_manager_password_confirm == "":
 326 +                 repl_manager_password_confirm = getpass("Confirm replication manager password: ")
 327 +             if repl_manager_password_confirm == repl_manager_password:
 328 +                 break
 329 +             else:
 330 +                 print("Passwords do not match!\n")
 331 +                 repl_manager_password = ""
 332 +                 repl_manager_password_confirm = ""
 333 + 
 334 +     manager = BootstrapReplicationManager(inst, dn=manager_dn)
 335 +     try:
 336 +         manager.create(properties={
 337 +             'cn': manager_cn,
 338 +             'userPassword': repl_manager_password
 339 +         })
 340 +         print ("Successfully created replication manager: " + manager_dn)
 341 +     except ldap.ALREADY_EXISTS:
 342 +         log.info("Replication Manager ({}) already exists".format(manager_dn))
 343 + 
 344 + 
 345 + def del_repl_manager(inst, basedn, log, args):
 346 +     if is_a_dn(agmt.name):
 347 +         manager_dn = args.name
 348 +     else:
 349 +         manager_dn = "cn={},cn=config".format(args.name)
 350 +     manager = BootstrapReplicationManager(inst, dn=manager_dn)
 351 +     manager.delete()
 352 +     print("Successfully deleted replication manager: " + manager_dn)
 353 + 
 354 + 
 355 + #
 356 + # Agreements
 357 + #
 358 + def list_agmts(inst, basedn, log, args):
 359 +     # List regular DS agreements
 360 +     replicas = Replicas(inst)
 361 +     replica = replicas.get(args.suffix)
 362 +     agmts = replica.get_agreements().list()
 363 + 
 364 +     result = {"type": "list", "items": []}
 365 +     for agmt in agmts:
 366 +         if args.json:
 367 +             entry = agmt.get_all_attrs_json()
 368 +             # Append decoded json object, because we are going to dump it later
 369 +             result['items'].append(json.loads(entry))
 370 +         else:
 371 +             print(agmt.display())
 372 +     if args.json:
 373 +         print(json.dumps(result))
 374 + 
 375 + 
 376 + def add_agmt(inst, basedn, log, args):
 377 +     repl_root = args.suffix
 378 +     bind_method = args.bind_method.lower()
 379 +     replicas = Replicas(inst)
 380 +     replica = replicas.get(args.suffix)
 381 +     agmts = replica.get_agreements()
 382 + 
 383 +     # Process fractional settings
 384 +     frac_list = None
 385 +     if args.frac_list:
 386 +         frac_list = "(objectclass=*) $ EXCLUDE"
 387 +         for attr in args.frac_list.split():
 388 +             frac_list += " " + attr
 389 + 
 390 +     frac_total_list = None
 391 +     if args.frac_list_total:
 392 +         frac_total_list = "(objectclass=*) $ EXCLUDE"
 393 +         for attr in args.frac_list_total.split():
 394 +             frac_total_list += " " + attr
 395 + 
 396 +     # Required properties
 397 +     properties = {
 398 +             'cn': args.AGMT_NAME[0],
 399 +             'nsDS5ReplicaRoot': repl_root,
 400 +             'description': args.AGMT_NAME[0],
 401 +             'nsDS5ReplicaHost': args.host,
 402 +             'nsDS5ReplicaPort': args.port,
 403 +             'nsDS5ReplicaBindMethod': bind_method,
 404 +             'nsDS5ReplicaTransportInfo': args.conn_protocol
 405 +         }
 406 + 
 407 +     # Add optional properties
 408 +     if args.bind_dn is not None:
 409 +         if not is_a_dn(args.bind_dn):
 410 +             raise ValueError("The replica bind DN is not a valid DN")
 411 +         properties['nsDS5ReplicaBindDN'] = args.bind_dn
 412 +     if args.bind_passwd is not None:
 413 +         properties['nsDS5ReplicaCredentials'] = args.bind_passwd
 414 +     if args.schedule is not None:
 415 +         properties['nsds5replicaupdateschedule'] = args.schedule
 416 +     if frac_list is not None:
 417 +         properties['nsds5replicatedattributelist'] = frac_list
 418 +     if frac_total_list is not None:
 419 +         properties['nsds5replicatedattributelisttotal'] = frac_total_list
 420 +     if args.strip_list is not None:
 421 +         properties['nsds5replicastripattrs'] = args.strip_list
 422 + 
 423 +     # We do need the bind dn and credentials for none-sasl bind methods
 424 +     if (bind_method == 'simple' or 'sslclientauth') and (args.bind_dn is None or args.bind_passwd is None):
 425 +         raise ValueError("You need to set the bind dn (--bind-dn) and the password (--bind-passwd) for bind method ({})".format(bind_method))
 426 + 
 427 +     # Create the agmt
 428 +     try:
 429 +         agmts.create(properties=properties)
 430 +     except ldap.ALREADY_EXISTS:
 431 +         raise ValueError("A replication agreement with the same name already exists")
 432 + 
 433 +     print("Successfully created replication agreement \"{}\"".format(args.AGMT_NAME[0]))
 434 +     if args.init:
 435 +         init_agmt(inst, basedn, log, args)
 436 + 
 437 + 
 438 + def delete_agmt(inst, basedn, log, args):
 439 +     agmt = get_agmt(inst, args)
 440 +     agmt.delete()
 441 +     print("Agreement has been successfully deleted")
 442 + 
 443 + 
 444 + def enable_agmt(inst, basedn, log, args):
 445 +     agmt = get_agmt(inst, args)
 446 +     agmt.resume()
 447 +     print("Agreement has been enabled")
 448 + 
 449 + 
 450 + def disable_agmt(inst, basedn, log, args):
 451 +     agmt = get_agmt(inst, args)
 452 +     agmt.pause()
 453 +     print("Agreement has been disabled")
 454 + 
 455 + 
 456 + def init_agmt(inst, basedn, log, args):
 457 +     agmt = get_agmt(inst, args)
 458 +     agmt.begin_reinit()
 459 +     print("Agreement initialization started...")
 460 + 
 461 + 
 462 + def check_init_agmt(inst, basedn, log, args):
 463 +     agmt = get_agmt(inst, args)
 464 +     (done, inprogress, error) = agmt.check_reinit()
 465 +     status = "Unknown"
 466 +     if done:
 467 +         status = "Agreement successfully initialized."
 468 +     elif inprogress:
 469 +         status = "Agreement initialization in progress."
 470 +     elif error:
 471 +         status = "Agreement initialization failed."
 472 +     if args.json:
 473 +         print(json.dumps(status))
 474 +     else:
 475 +         print(status)
 476 + 
 477 + 
 478 + def set_agmt(inst, basedn, log, args):
 479 +     agmt = get_agmt(inst, args)
 480 +     attrs = _args_to_attrs(args)
 481 +     modlist = []
 482 +     for attr, value in attrs.items():
 483 +         modlist.append((attr, value))
 484 +     if len(modlist) > 0:
 485 +         agmt.replace_many(*modlist)
 486 +     else:
 487 +         raise ValueError("There are no changes to set in the agreement")
 488 +     print("Successfully updated agreement")
 489 + 
 490 + 
 491 + def get_repl_agmt(inst, basedn, log, args):
 492 +     agmt = get_agmt(inst, args)
 493 +     if args.json:
 494 +         print(agmt.get_all_attrs_json())
 495 +     else:
 496 +         print(agmt.display())
 497 + 
 498 + 
 499 + def poke_agmt(inst, basedn, log, args):
 500 +     # Send updates now
 501 +     agmt = get_agmt(inst, args)
 502 +     agmt.pause()
 503 +     agmt.resume()
 504 +     print("Agreement has been poked")
 505 + 
 506 + 
 507 + def get_agmt_status(inst, basedn, log, args):
 508 +     agmt = get_agmt(inst, args)
 509 +     if args.bind_dn is not None and args.bind_passwd is None:
 510 +         args.bind_passwd = ""
 511 +         while args.bind_passwd == "":
 512 +             args.bind_passwd = getpass("Enter password for \"{}\": ".format(args.bind_dn))
 513 +     status = agmt.status(use_json=args.json, binddn=args.bind_dn, bindpw=args.bind_passwd)
 514 +     print(agmt, status)
 515 + 
 516 + 
 517 + #
 518 + # Winsync agreement specfic functions
 519 + #
 520 + def list_winsync_agmts(inst, basedn, log, args):
 521 +     # List regular DS agreements
 522 +     replicas = Replicas(inst)
 523 +     replica = replicas.get(args.suffix)
 524 +     agmts = replica.get_agreements(winsync=True).list()
 525 + 
 526 +     result = {"type": "list", "items": []}
 527 +     for agmt in agmts:
 528 +         if args.json:
 529 +             entry = agmt.get_all_attrs_json()
 530 +             # Append decoded json object, because we are going to dump it later
 531 +             result['items'].append(json.loads(entry))
 532 +         else:
 533 +             print(agmt.display())
 534 +     if args.json:
 535 +         print(json.dumps(result))
 536 + 
 537 + 
 538 + def add_winsync_agmt(inst, basedn, log, args):
 539 +     replicas = Replicas(inst)
 540 +     replica = replicas.get(args.suffix)
 541 +     agmts = replica.get_agreements(winsync=True)
 542 + 
 543 +     # Process fractional settings
 544 +     frac_list = None
 545 +     if args.frac_list:
 546 +         frac_list = "(objectclass=*) $ EXCLUDE"
 547 +         for attr in args.frac_list.split():
 548 +             frac_list += " " + attr
 549 + 
 550 +     if not is_a_dn(args.bind_dn):
 551 +         raise ValueError("The replica bind DN is not a valid DN")
 552 + 
 553 +     # Required properties
 554 +     properties = {
 555 +             'cn': args.AGMT_NAME[0],
 556 +             'nsDS5ReplicaRoot': args.suffix,
 557 +             'description': args.AGMT_NAME[0],
 558 +             'nsDS5ReplicaHost': args.host,
 559 +             'nsDS5ReplicaPort': args.port,
 560 +             'nsDS5ReplicaTransportInfo': args.conn_protocol,
 561 +             'nsDS5ReplicaBindDN': args.bind_dn,
 562 +             'nsDS5ReplicaCredentials': args.bind_passwd,
 563 +             'nsds7windowsreplicasubtree': args.win_subtree,
 564 +             'nsds7directoryreplicasubtree': args.ds_subtree,
 565 +             'nsds7windowsDomain': args.win_domain,
 566 +         }
 567 + 
 568 +     # Add optional properties
 569 +     if args.sync_users is not None:
 570 +         properties['nsds7newwinusersyncenabled'] = args.sync_users
 571 +     if args.sync_groups is not None:
 572 +         properties['nsds7newwingroupsyncenabled'] = args.sync_groups
 573 +     if args.sync_interval is not None:
 574 +         properties['winsyncinterval'] = args.sync_interval
 575 +     if args.one_way_sync is not None:
 576 +         properties['onewaysync'] = args.one_way_sync
 577 +     if args.move_action is not None:
 578 +         properties['winsyncmoveAction'] = args.move_action
 579 +     if args.ds_filter is not None:
 580 +         properties['winsyncdirectoryfilter'] = args.ds_filter
 581 +     if args.win_filter is not None:
 582 +         properties['winsyncwindowsfilter'] = args.win_filter
 583 +     if args.schedule is not None:
 584 +         properties['nsds5replicaupdateschedule'] = args.schedule
 585 +     if frac_list is not None:
 586 +         properties['nsds5replicatedattributelist'] = frac_list
 587 + 
 588 +     # Create the agmt
 589 +     try:
 590 +         agmts.create(properties=properties)
 591 +     except ldap.ALREADY_EXISTS:
 592 +         raise ValueError("A replication agreement with the same name already exists")
 593 + 
 594 +     print("Successfully created winsync replication agreement \"{}\"".format(args.AGMT_NAME[0]))
 595 +     if args.init:
 596 +         init_winsync_agmt(inst, basedn, log, args)
 597 + 
 598 + 
 599 + def delete_winsync_agmt(inst, basedn, log, args):
 600 +     agmt = get_agmt(inst, args, winsync=True)
 601 +     agmt.delete()
 602 +     print("Agreement has been successfully deleted")
 603 + 
 604 + 
 605 + def set_winsync_agmt(inst, basedn, log, args):
 606 +     agmt = get_agmt(inst, args, winsync=True)
 607 + 
 608 +     attrs = _args_to_attrs(args)
 609 +     modlist = []
 610 +     for attr, value in attrs.items():
 611 +         modlist.append((attr, value))
 612 +     if len(modlist) > 0:
 613 +         agmt.replace_many(*modlist)
 614 +     else:
 615 +         raise ValueError("There are no changes to set in the agreement")
 616 +     print("Successfully updated agreement")
 617 + 
 618 + 
 619 + def enable_winsync_agmt(inst, basedn, log, args):
 620 +     agmt = get_agmt(inst, args, winsync=True)
 621 +     agmt.resume()
 622 +     print("Agreement has been enabled")
 623 + 
 624 + 
 625 + def disable_winsync_agmt(inst, basedn, log, args):
 626 +     agmt = get_agmt(inst, args, winsync=True)
 627 +     agmt.pause()
 628 +     print("Agreement has been disabled")
 629 + 
 630 + 
 631 + def init_winsync_agmt(inst, basedn, log, args):
 632 +     agmt = get_agmt(inst, args, winsync=True)
 633 +     agmt.begin_reinit()
 634 +     print("Agreement initialization started...")
 635 + 
 636 + 
 637 + def check_winsync_init_agmt(inst, basedn, log, args):
 638 +     agmt = get_agmt(inst, args, winsync=True)
 639 +     (done, inprogress, error) = agmt.check_reinit()
 640 +     status = "Unknown"
 641 +     if done:
 642 +         status = "Agreement successfully initialized."
 643 +     elif inprogress:
 644 +         status = "Agreement initialization in progress."
 645 +     elif error:
 646 +         status = "Agreement initialization failed."
 647 +     if args.json:
 648 +         print(json.dumps(status))
 649 +     else:
 650 +         print(status)
 651 + 
 652 + 
 653 + def get_winsync_agmt(inst, basedn, log, args):
 654 +     agmt = get_agmt(inst, args, winsync=True)
 655 +     if args.json:
 656 +         print(agmt.get_all_attrs_json())
 657 +     else:
 658 +         print(agmt.display())
 659 + 
 660 + 
 661 + def poke_winsync_agmt(inst, basedn, log, args):
 662 +     # Send updates now
 663 +     agmt = get_agmt(inst, args, winsync=True)
 664 +     agmt.pause()
 665 +     agmt.resume()
 666 +     print("Agreement has been poked")
 667 + 
 668 + 
 669 + def get_winsync_agmt_status(inst, basedn, log, args):
 670 +     agmt = get_agmt(inst, args, winsync=True)
 671 +     status = agmt.status(winsync=True, use_json=args.json)
 672 +     print(agmt, status)
 673 + 
 674 + 
 675 + #
 676 + # Tasks
 677 + #
 678 + def run_cleanallruv(inst, basedn, log, args):
 679 +     properties = {'replica-base-dn': args.suffix,
 680 +                   'replica-id': args.replica_id}
 681 +     if args.force_cleaning:
 682 +         properties['replica-force-cleaning'] = args.force_cleaning
 683 +     clean_task = CleanAllRUVTask(inst)
 684 +     clean_task.create(properties=properties)
 685 + 
 686 + 
 687 + def abort_cleanallruv(inst, basedn, log, args):
 688 +     properties = {'replica-base-dn': args.suffix,
 689 +                   'replica-id': args.replica_id}
 690 +     if args.certify:
 691 +         properties['replica-certify-all'] = args.certify
 692 +     clean_task = AbortCleanAllRUVTask(inst)
 693 +     clean_task.create(properties=properties)
 694 + 
 695 + 
 696 + def create_parser(subparsers):
 697 + 
 698 +     ############################################
 699 +     # Replication Configuration
 700 +     ############################################
 701 + 
 702 +     repl_parser = subparsers.add_parser('replication', help='Configure replication for a suffix')
 703 +     repl_subcommands = repl_parser.add_subparsers(help='Replication Configuration')
 704 + 
 705 +     repl_enable_parser = repl_subcommands.add_parser('enable', help='Enable replication for a suffix')
 706 +     repl_enable_parser.set_defaults(func=enable_replication)
 707 +     repl_enable_parser.add_argument('--suffix', required=True, help='The DN of the suffix to be enabled for replication')
 708 +     repl_enable_parser.add_argument('--role', required=True, help="The Replication role: \"master\", \"hub\", or \"consumer\"")
 709 +     repl_enable_parser.add_argument('--replica-id', help="The replication identifier for a \"master\".  Values range from 1 - 65534")
 710 +     repl_enable_parser.add_argument('--bind-group-dn', help="A group entry DN containing members that are \"bind/supplier\" DNs")
 711 +     repl_enable_parser.add_argument('--bind-dn', help="The Bind or Supplier DN that can make replication updates")
 712 + 
 713 +     repl_disable_parser = repl_subcommands.add_parser('disable', help='Disable replication for a suffix')
 714 +     repl_disable_parser.set_defaults(func=disable_replication)
 715 +     repl_disable_parser.add_argument('--suffix', required=True, help='The DN of the suffix to have replication disabled')
 716 + 
 717 +     repl_promote_parser = repl_subcommands.add_parser('promote', help='Promte replica to a Hub or Master')
 718 +     repl_promote_parser.set_defaults(func=promote_replica)
 719 +     repl_promote_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix to promote")
 720 +     repl_promote_parser.add_argument('--newrole', required=True, help='Promote this replica to a \"hub\" or \"master\"')
 721 +     repl_promote_parser.add_argument('--replica-id', help="The replication identifier for a \"master\".  Values range from 1 - 65534")
 722 +     repl_promote_parser.add_argument('--bind-group-dn', help="A group entry DN containing members that are \"bind/supplier\" DNs")
 723 +     repl_promote_parser.add_argument('--bind-dn', help="The Bind or Supplier DN that can make replication updates")
 724 + 
 725 +     repl_add_manager_parser = repl_subcommands.add_parser('create-manager', help='Create a replication manager entry')
 726 +     repl_add_manager_parser.set_defaults(func=create_repl_manager)
 727 +     repl_add_manager_parser.add_argument('--name', help="The NAME of the new replication manager entry under cn=config:  \"cn=NAME,cn=config\"")
 728 +     repl_add_manager_parser.add_argument('--passwd', help="Password for replication manager.  If not provided, you will be prompted for the password")
 729 + 
 730 +     repl_del_manager_parser = repl_subcommands.add_parser('delete-manager', help='Delete a replication manager entry')
 731 +     repl_del_manager_parser.set_defaults(func=del_repl_manager)
 732 +     repl_del_manager_parser.add_argument('--name', help="The NAME of the replication manager entry under cn=config:  \"cn=NAME,cn=config\"")
 733 + 
 734 +     repl_demote_parser = repl_subcommands.add_parser('demote', help='Demote replica to a Hub or Consumer')
 735 +     repl_demote_parser.set_defaults(func=demote_replica)
 736 +     repl_demote_parser.add_argument('--suffix', required=True, help="Promte this replica to a \"hub\" or \"consumer\"")
 737 +     repl_demote_parser.add_argument('--newrole', required=True, help="The Replication role: \"hub\", or \"consumer\"")
 738 + 
 739 +     repl_get_parser = repl_subcommands.add_parser('get', help='Get replication configuration')
 740 +     repl_get_parser.set_defaults(func=get_repl_config)
 741 +     repl_get_parser.add_argument('--suffix', required=True, help='Get the replication configuration for this suffix DN')
 742 + 
 743 +     repl_create_cl = repl_subcommands.add_parser('create-changelog', help='Create the replication changelog')
 744 +     repl_create_cl.set_defaults(func=create_cl)
 745 + 
 746 +     repl_delete_cl = repl_subcommands.add_parser('delete-changelog', help='Delete the replication changelog.  This will invalidate any existing replication agreements')
 747 +     repl_delete_cl.set_defaults(func=delete_cl)
 748 + 
 749 +     repl_set_cl = repl_subcommands.add_parser('set-changelog', help='Delete the replication changelog.  This will invalidate any existing replication agreements')
 750 +     repl_set_cl.set_defaults(func=set_cl)
 751 +     repl_set_cl.add_argument('--max-entries', help="The maximum number of entries to get in the replication changelog")
 752 +     repl_set_cl.add_argument('--max-age', help="The maximum age of a replication changelog entry")
 753 +     repl_set_cl.add_argument('--compact-interval', help="The replication changelog compaction interval")
 754 +     repl_set_cl.add_argument('--trim-interval', help="The interval to check if the replication changelog can be trimmed")
 755 +     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")
 756 +     repl_set_cl.add_argument('--encrypt-key', help="The symmetric key for the replication changelog encryption")
 757 + 
 758 +     repl_get_cl = repl_subcommands.add_parser('get-changelog', help='Delete the replication changelog.  This will invalidate any existing replication agreements')
 759 +     repl_get_cl.set_defaults(func=get_cl)
 760 + 
 761 +     repl_set_parser = repl_subcommands.add_parser('set', help='Set an attribute in the replication configuration')
 762 +     repl_set_parser.set_defaults(func=set_repl_config)
 763 +     repl_set_parser.add_argument('--suffix', required=True, help='The DN of the replication suffix')
 764 +     repl_set_parser.add_argument('--replica-id', help="The Replication Identifier number")
 765 +     repl_set_parser.add_argument('--replica-role', help="The Replication role: master, hub, or consumer")
 766 + 
 767 +     repl_set_parser.add_argument('--repl-add-bind-dn', help="Add a bind (supplier) DN")
 768 +     repl_set_parser.add_argument('--repl-del-bind-dn', help="Remove a bind (supplier) DN")
 769 +     repl_set_parser.add_argument('--repl-add-ref', help="Add a replication referral (for consumers only)")
 770 +     repl_set_parser.add_argument('--repl-del-ref', help="Remove a replication referral (for conusmers only)")
 771 +     repl_set_parser.add_argument('--repl-purge-delay', help="The replication purge delay")
 772 +     repl_set_parser.add_argument('--repl-tombstone-purge-interval', help="The interval in seconds to check for tombstones that can be purged")
 773 +     repl_set_parser.add_argument('--repl-fast-tombstone-purging', help="Set to \"on\" to improve tombstone purging performance")
 774 +     repl_set_parser.add_argument('--repl-bind-group', help="A group entry DN containing members that are \"bind/supplier\" DNs")
 775 +     repl_set_parser.add_argument('--repl-bind-group-interval', help="An interval in seconds to check if the bind group has been updated")
 776 +     repl_set_parser.add_argument('--repl-protocol-timeout', help="A timeout in seconds on how long to wait before stopping "
 777 +                                                                  "replication when the server is under load")
 778 +     repl_set_parser.add_argument('--repl-backoff-max', help="The maximum time in seconds a replication agreement should stay in a backoff state "
 779 +                                                             "while waiting to acquire the consumer.  Default is 300 seconds")
 780 +     repl_set_parser.add_argument('--repl-backoff-min', help="The starting time in seconds a replication agreement should stay in a backoff state "
 781 +                                                             "while waiting to acquire the consumer.  Default is 3 seconds")
 782 +     repl_set_parser.add_argument('--repl-release-timeout', help="A timeout in seconds a replication master should send "
 783 +                                                                 "updates before it yields its replication session")
 784 + 
 785 +     ############################################
 786 +     # Replication Agmts
 787 +     ############################################
 788 + 
 789 +     agmt_parser = subparsers.add_parser('repl-agmt', help='Manage replication agreements')
 790 +     agmt_subcommands = agmt_parser.add_subparsers(help='Replication Agreement Configuration')
 791 + 
 792 +     # List
 793 +     agmt_list_parser = agmt_subcommands.add_parser('list', help='List all the replication agreements')
 794 +     agmt_list_parser.set_defaults(func=list_agmts)
 795 +     agmt_list_parser.add_argument('--suffix', required=True, help='The DN of the suffix to look up replication agreements')
 796 +     agmt_list_parser.add_argument('--entry', help='Return the entire entry for each agreement')
 797 + 
 798 +     # Enable
 799 +     agmt_enable_parser = agmt_subcommands.add_parser('enable', help='Enable replication agreement')
 800 +     agmt_enable_parser.set_defaults(func=enable_agmt)
 801 +     agmt_enable_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement')
 802 +     agmt_enable_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix")
 803 + 
 804 +     # Disable
 805 +     agmt_disable_parser = agmt_subcommands.add_parser('disable', help='Disable replication agreement')
 806 +     agmt_disable_parser.set_defaults(func=disable_agmt)
 807 +     agmt_disable_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement')
 808 +     agmt_disable_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix")
 809 + 
 810 +     # Initialize
 811 +     agmt_init_parser = agmt_subcommands.add_parser('init', help='Initialize replication agreement')
 812 +     agmt_init_parser.set_defaults(func=init_agmt)
 813 +     agmt_init_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement')
 814 +     agmt_init_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix")
 815 + 
 816 +     # Check Initialization progress
 817 +     agmt_check_init_parser = agmt_subcommands.add_parser('init-status', help='Check the agreement initialization status')
 818 +     agmt_check_init_parser.set_defaults(func=check_init_agmt)
 819 +     agmt_check_init_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement')
 820 +     agmt_check_init_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix")
 821 + 
 822 +     # Send Updates Now
 823 +     agmt_poke_parser = agmt_subcommands.add_parser('poke', help='Trigger replication to send updates now')
 824 +     agmt_poke_parser.set_defaults(func=poke_agmt)
 825 +     agmt_poke_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement')
 826 +     agmt_poke_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix")
 827 + 
 828 +     # Status
 829 +     agmt_status_parser = agmt_subcommands.add_parser('status', help='Get the current status of the replication agreement')
 830 +     agmt_status_parser.set_defaults(func=get_agmt_status)
 831 +     agmt_status_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement')
 832 +     agmt_status_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix")
 833 +     agmt_status_parser.add_argument('--bind-dn', help="Set the DN to bind to the consumer")
 834 +     agmt_status_parser.add_argument('--bind-passwd', help="The password for the bind DN")
 835 + 
 836 +     # Delete
 837 +     agmt_del_parser = agmt_subcommands.add_parser('delete', help='Delete replication agreement')
 838 +     agmt_del_parser.set_defaults(func=delete_agmt)
 839 +     agmt_del_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement')
 840 +     agmt_del_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix")
 841 + 
 842 +     # Create
 843 +     agmt_add_parser = agmt_subcommands.add_parser('create', help='Initialize replication agreement')
 844 +     agmt_add_parser.set_defaults(func=add_agmt)
 845 +     agmt_add_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement')
 846 +     agmt_add_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix")
 847 +     agmt_add_parser.add_argument('--host', required=True, help="The hostname of the remote replica")
 848 +     agmt_add_parser.add_argument('--port', required=True, help="The port number of the remote replica")
 849 +     agmt_add_parser.add_argument('--conn-protocol', required=True, help="The replication connection protocol: LDAP, LDAPS, or StartTLS")
 850 +     agmt_add_parser.add_argument('--bind-dn', help="The Bind DN the agreement uses to authenticate to the replica")
 851 +     agmt_add_parser.add_argument('--bind-passwd', help="The credentials for the Bind DN")
 852 +     agmt_add_parser.add_argument('--bind-method', required=True, help="The bind method: \"SIMPLE\", \"SSLCLIENTAUTH\", \"SASL/DIGEST\", or \"SASL/GSSAPI\"")
 853 +     agmt_add_parser.add_argument('--frac-list', help="List of attributes to NOT replicate to the consumer during incremental updates")
 854 +     agmt_add_parser.add_argument('--frac-list-total', help="List of attributes to NOT replicate during a total initialization")
 855 +     agmt_add_parser.add_argument('--strip-list', help="A list of attributes that are removed from updates only if the event "
 856 +                                                       "would otherwise be empty.  Typically this is set to \"modifiersname\" and \"modifytimestmap\"")
 857 +     agmt_add_parser.add_argument('--schedule', help="Sets the replication update schedule: 'HHMM-HHMM DDDDDDD'  D = 0-6 (Sunday - Saturday).")
 858 +     agmt_add_parser.add_argument('--conn-timeout', help="The timeout used for replicaton connections")
 859 +     agmt_add_parser.add_argument('--protocol-timeout', help="A timeout in seconds on how long to wait before stopping "
 860 +                                                             "replication when the server is under load")
 861 +     agmt_add_parser.add_argument('--wait-async-results', help="The amount of time in milliseconds the server waits if "
 862 +                                                               "the consumer is not ready before resending data")
 863 +     agmt_add_parser.add_argument('--busy-wait-time', help="The amount of time in seconds a supplier should wait after "
 864 +                                                           "a consumer sends back a busy response before making another "
 865 +                                                           "attempt to acquire access.")
 866 +     agmt_add_parser.add_argument('--session-pause-time', help="The amount of time in seconds a supplier should wait between update sessions.")
 867 +     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.")
 868 +     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\"")
 869 +     agmt_add_parser.add_argument('--init', action='store_true', default=False, help="Initialize the agreement after creating it.")
 870 + 
 871 +     # Set - Note can not use add's parent args because for "set" there are no "required=True" args
 872 +     agmt_set_parser = agmt_subcommands.add_parser('set', help='Set an attribute in the replication agreement')
 873 +     agmt_set_parser.set_defaults(func=set_agmt)
 874 +     agmt_set_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement')
 875 +     agmt_set_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix")
 876 +     agmt_set_parser.add_argument('--host', help="The hostname of the remote replica")
 877 +     agmt_set_parser.add_argument('--port', help="The port number of the remote replica")
 878 +     agmt_set_parser.add_argument('--conn-protocol', help="The replication connection protocol: LDAP, LDAPS, or StartTLS")
 879 +     agmt_set_parser.add_argument('--bind-dn', help="The Bind DN the agreement uses to authenticate to the replica")
 880 +     agmt_set_parser.add_argument('--bind-passwd', help="The credentials for the Bind DN")
 881 +     agmt_set_parser.add_argument('--bind-method', help="The bind method: \"SIMPLE\", \"SSLCLIENTAUTH\", \"SASL/DIGEST\", or \"SASL/GSSAPI\"")
 882 +     agmt_set_parser.add_argument('--frac-list', help="List of attributes to NOT replicate to the consumer during incremental updates")
 883 +     agmt_set_parser.add_argument('--frac-list-total', help="List of attributes to NOT replicate during a total initialization")
 884 +     agmt_set_parser.add_argument('--strip-list', help="A list of attributes that are removed from updates only if the event "
 885 +                                                       "would otherwise be empty.  Typically this is set to \"modifiersname\" and \"modifytimestmap\"")
 886 +     agmt_set_parser.add_argument('--schedule', help="Sets the replication update schedule: 'HHMM-HHMM DDDDDDD'  D = 0-6 (Sunday - Saturday).")
 887 +     agmt_set_parser.add_argument('--conn-timeout', help="The timeout used for replicaton connections")
 888 +     agmt_set_parser.add_argument('--protocol-timeout', help="A timeout in seconds on how long to wait before stopping "
 889 +                                                             "replication when the server is under load")
 890 +     agmt_set_parser.add_argument('--wait-async-results', help="The amount of time in milliseconds the server waits if "
 891 +                                                               "the consumer is not ready before resending data")
 892 +     agmt_set_parser.add_argument('--busy-wait-time', help="The amount of time in seconds a supplier should wait after "
 893 +                                                           "a consumer sends back a busy response before making another "
 894 +                                                           "attempt to acquire access.")
 895 +     agmt_set_parser.add_argument('--session-pause-time', help="The amount of time in seconds a supplier should wait between update sessions.")
 896 +     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.")
 897 +     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\"")
 898 + 
 899 +     # Get
 900 +     agmt_get_parser = agmt_subcommands.add_parser('get', help='Get replication configuration')
 901 +     agmt_get_parser.set_defaults(func=get_repl_agmt)
 902 +     agmt_get_parser.add_argument('AGMT_NAME', nargs=1, help='Get the replication configuration for this suffix DN')
 903 +     agmt_get_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix")
 904 + 
 905 +     ############################################
 906 +     # Replication Winsync Agmts
 907 +     ############################################
 908 + 
 909 +     winsync_parser = subparsers.add_parser('repl-winsync-agmt', help='Manage Winsync Agreements')
 910 +     winsync_agmt_subcommands = winsync_parser.add_subparsers(help='Replication Winsync Agreement Configuration')
 911 + 
 912 +     # List
 913 +     winsync_agmt_list_parser = winsync_agmt_subcommands.add_parser('list', help='List all the replication winsync agreements')
 914 +     winsync_agmt_list_parser.set_defaults(func=list_winsync_agmts)
 915 +     winsync_agmt_list_parser.add_argument('--suffix', required=True, help='The DN of the suffix to look up replication winsync agreements')
 916 + 
 917 +     # Enable
 918 +     winsync_agmt_enable_parser = winsync_agmt_subcommands.add_parser('enable', help='Enable replication winsync agreement')
 919 +     winsync_agmt_enable_parser.set_defaults(func=enable_winsync_agmt)
 920 +     winsync_agmt_enable_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement')
 921 +     winsync_agmt_enable_parser.add_argument('--suffix', required=True, help="The DN of the replication winsync suffix")
 922 + 
 923 +     # Disable
 924 +     winsync_agmt_disable_parser = winsync_agmt_subcommands.add_parser('disable', help='Disable replication winsync agreement')
 925 +     winsync_agmt_disable_parser.set_defaults(func=disable_winsync_agmt)
 926 +     winsync_agmt_disable_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement')
 927 +     winsync_agmt_disable_parser.add_argument('--suffix', required=True, help="The DN of the replication winsync suffix")
 928 + 
 929 +     # Initialize
 930 +     winsync_agmt_init_parser = winsync_agmt_subcommands.add_parser('init', help='Initialize replication winsync agreement')
 931 +     winsync_agmt_init_parser.set_defaults(func=init_winsync_agmt)
 932 +     winsync_agmt_init_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement')
 933 +     winsync_agmt_init_parser.add_argument('--suffix', required=True, help="The DN of the replication winsync suffix")
 934 + 
 935 +     # Check Initialization progress
 936 +     winsync_agmt_check_init_parser = winsync_agmt_subcommands.add_parser('init-status', help='Check the agreement initialization status')
 937 +     winsync_agmt_check_init_parser.set_defaults(func=check_winsync_init_agmt)
 938 +     winsync_agmt_check_init_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement')
 939 +     winsync_agmt_check_init_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix")
 940 + 
 941 +     # Send Updates Now
 942 +     winsync_agmt_poke_parser = winsync_agmt_subcommands.add_parser('poke', help='Trigger replication to send updates now')
 943 +     winsync_agmt_poke_parser.set_defaults(func=poke_winsync_agmt)
 944 +     winsync_agmt_poke_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement')
 945 +     winsync_agmt_poke_parser.add_argument('--suffix', required=True, help="The DN of the replication winsync suffix")
 946 + 
 947 +     # Status
 948 +     winsync_agmt_status_parser = winsync_agmt_subcommands.add_parser('status', help='Get the current status of the replication agreement')
 949 +     winsync_agmt_status_parser.set_defaults(func=get_winsync_agmt_status)
 950 +     winsync_agmt_status_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement')
 951 +     winsync_agmt_status_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix")
 952 + 
 953 +     # Delete
 954 +     winsync_agmt_del_parser = winsync_agmt_subcommands.add_parser('delete', help='Delete replication winsync agreement')
 955 +     winsync_agmt_del_parser.set_defaults(func=delete_winsync_agmt)
 956 +     winsync_agmt_del_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement')
 957 +     winsync_agmt_del_parser.add_argument('--suffix', required=True, help="The DN of the replication winsync suffix")
 958 + 
 959 +     # Create
 960 +     winsync_agmt_add_parser = winsync_agmt_subcommands.add_parser('create', help='Initialize replication winsync agreement')
 961 +     winsync_agmt_add_parser.set_defaults(func=add_winsync_agmt)
 962 +     winsync_agmt_add_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement')
 963 +     winsync_agmt_add_parser.add_argument('--suffix', required=True, help="The DN of the replication winsync suffix")
 964 +     winsync_agmt_add_parser.add_argument('--host', required=True, help="The hostname of the AD server")
 965 +     winsync_agmt_add_parser.add_argument('--port', required=True, help="The port number of the AD server")
 966 +     winsync_agmt_add_parser.add_argument('--conn-protocol', required=True, help="The replication winsync connection protocol: LDAP, LDAPS, or StartTLS")
 967 +     winsync_agmt_add_parser.add_argument('--bind-dn', required=True, help="The Bind DN the agreement uses to authenticate to the AD Server")
 968 +     winsync_agmt_add_parser.add_argument('--bind-passwd', required=True, help="The credentials for the Bind DN")
 969 +     winsync_agmt_add_parser.add_argument('--frac-list', help="List of attributes to NOT replicate to the consumer during incremental updates")
 970 +     winsync_agmt_add_parser.add_argument('--schedule', help="Sets the replication update schedule")
 971 +     winsync_agmt_add_parser.add_argument('--win-subtree', required=True, help="The suffix of the AD Server")
 972 +     winsync_agmt_add_parser.add_argument('--ds-subtree', required=True, help="The Directory Server suffix")
 973 +     winsync_agmt_add_parser.add_argument('--win-domain', required=True, help="The AD Domain")
 974 +     winsync_agmt_add_parser.add_argument('--sync-users', help="Synchronize Users between AD and DS")
 975 +     winsync_agmt_add_parser.add_argument('--sync-groups', help="Synchronize Groups between AD and DS")
 976 +     winsync_agmt_add_parser.add_argument('--sync-interval', help="The interval that DS checks AD for changes in entries")
 977 +     winsync_agmt_add_parser.add_argument('--one-way-sync', help="Sets which direction to perform synchronization: \"toWindows\", \"fromWindows\", \"both\"")
 978 +     winsync_agmt_add_parser.add_argument('--move-action', help="Sets instructions on how to handle moved or deleted entries: \"none\", \"unsync\", or \"delete\"")
 979 +     winsync_agmt_add_parser.add_argument('--win-filter', help="Custom filter for finding users in AD Server")
 980 +     winsync_agmt_add_parser.add_argument('--ds-filter', help="Custom filter for finding AD users in DS Server")
 981 +     winsync_agmt_add_parser.add_argument('--subtree-pair', help="Set the subtree pair: <DS_SUBTREE>:<WINDOWS_SUBTREE>")
 982 +     winsync_agmt_add_parser.add_argument('--conn-timeout', help="The timeout used for replicaton connections")
 983 +     winsync_agmt_add_parser.add_argument('--busy-wait-time', help="The amount of time in seconds a supplier should wait after "
 984 +                                                           "a consumer sends back a busy response before making another "
 985 +                                                           "attempt to acquire access.")
 986 +     winsync_agmt_add_parser.add_argument('--session-pause-time', help="The amount of time in seconds a supplier should wait between update sessions.")
 987 +     winsync_agmt_add_parser.add_argument('--init', action='store_true', default=False, help="Initialize the agreement after creating it.")
 988 + 
 989 +     # Set - Note can not use add's parent args because for "set" there are no "required=True" args
 990 +     winsync_agmt_set_parser = winsync_agmt_subcommands.add_parser('set', help='Set an attribute in the replication winsync agreement')
 991 +     winsync_agmt_set_parser.set_defaults(func=set_winsync_agmt)
 992 +     winsync_agmt_set_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication winsync agreement')
 993 +     winsync_agmt_set_parser.add_argument('--suffix', help="The DN of the replication winsync suffix")
 994 +     winsync_agmt_set_parser.add_argument('--host', help="The hostname of the AD server")
 995 +     winsync_agmt_set_parser.add_argument('--port', help="The port number of the AD server")
 996 +     winsync_agmt_set_parser.add_argument('--conn-protocol', help="The replication winsync connection protocol: LDAP, LDAPS, or StartTLS")
 997 +     winsync_agmt_set_parser.add_argument('--bind-dn', help="The Bind DN the agreement uses to authenticate to the AD Server")
 998 +     winsync_agmt_set_parser.add_argument('--bind-passwd', help="The credentials for the Bind DN")
 999 +     winsync_agmt_set_parser.add_argument('--frac-list', help="List of attributes to NOT replicate to the consumer during incremental updates")
1000 +     winsync_agmt_set_parser.add_argument('--schedule', help="Sets the replication update schedule")
1001 +     winsync_agmt_set_parser.add_argument('--win-subtree', help="The suffix of the AD Server")
1002 +     winsync_agmt_set_parser.add_argument('--ds-subtree', help="The Directory Server suffix")
1003 +     winsync_agmt_set_parser.add_argument('--win-domain', help="The AD Domain")
1004 +     winsync_agmt_set_parser.add_argument('--sync-users', help="Synchronize Users between AD and DS")
1005 +     winsync_agmt_set_parser.add_argument('--sync-groups', help="Synchronize Groups between AD and DS")
1006 +     winsync_agmt_set_parser.add_argument('--sync-interval', help="The interval that DS checks AD for changes in entries")
1007 +     winsync_agmt_set_parser.add_argument('--one-way-sync', help="Sets which direction to perform synchronization: \"toWindows\", \"fromWindows\", \"both\"")
1008 +     winsync_agmt_set_parser.add_argument('--move-action', help="Sets instructions on how to handle moved or deleted entries: \"none\", \"unsync\", or \"delete\"")
1009 +     winsync_agmt_set_parser.add_argument('--win-filter', help="Custom filter for finding users in AD Server")
1010 +     winsync_agmt_set_parser.add_argument('--ds-filter', help="Custom filter for finding AD users in DS Server")
1011 +     winsync_agmt_set_parser.add_argument('--subtree-pair', help="Set the subtree pair: <DS_SUBTREE>:<WINDOWS_SUBTREE>")
1012 +     winsync_agmt_set_parser.add_argument('--conn-timeout', help="The timeout used for replicaton connections")
1013 +     winsync_agmt_set_parser.add_argument('--busy-wait-time', help="The amount of time in seconds a supplier should wait after "
1014 +                                                           "a consumer sends back a busy response before making another "
1015 +                                                           "attempt to acquire access.")
1016 +     winsync_agmt_set_parser.add_argument('--session-pause-time', help="The amount of time in seconds a supplier should wait between update sessions.")
1017 + 
1018 +     # Get
1019 +     winsync_agmt_get_parser = winsync_agmt_subcommands.add_parser('get', help='Get replication configuration')
1020 +     winsync_agmt_get_parser.set_defaults(func=get_winsync_agmt)
1021 +     winsync_agmt_get_parser.add_argument('AGMT_NAME', nargs=1, help='Get the replication configuration for this suffix DN')
1022 +     winsync_agmt_get_parser.add_argument('--suffix', required=True, help="The DN of the replication suffix")
1023 + 
1024 +     ############################################
1025 +     # Replication Tasks (cleanalruv)
1026 +     ############################################
1027 + 
1028 +     tasks_parser = subparsers.add_parser('repl-tasks', help='Manage local (user/subtree) password policies')
1029 +     task_subcommands = tasks_parser.add_subparsers(help='Replication Tasks')
1030 + 
1031 +     # Cleanallruv
1032 +     task_cleanallruv = task_subcommands.add_parser('cleanallruv', help='Cleanup old/removed replica IDs')
1033 +     task_cleanallruv.set_defaults(func=run_cleanallruv)
1034 +     task_cleanallruv.add_argument('--suffix', required=True, help="The Directory Server suffix")
1035 +     task_cleanallruv.add_argument('--replica-id', required=True, help="The replica ID to remove/clean")
1036 +     task_cleanallruv.add_argument('--force-cleaning', action='store_true', default=False,
1037 +                                   help="Ignore errors and do a best attempt to clean all the replicas")
1038 + 
1039 +     # Abort cleanallruv
1040 +     task_abort_cleanallruv = task_subcommands.add_parser('abort-cleanallruv', help='Set an attribute in the replication winsync agreement')
1041 +     task_abort_cleanallruv.set_defaults(func=abort_cleanallruv)
1042 +     task_abort_cleanallruv.add_argument('--suffix', required=True, help="The Directory Server suffix")
1043 +     task_abort_cleanallruv.add_argument('--replica-id', required=True, help="The replica ID of the cleaning task to abort")
1044 +     task_abort_cleanallruv.add_argument('--certify', action='store_true', default=False,
1045 +                                         help="Enforce that the abort task completed on all replicas")
1046 + 
1047 + 
 1 @@ -7,14 +7,11 @@
 2   # --- END COPYRIGHT BLOCK ---
 3   
 4   import ldap
 5 - import json
 6 - from ldap import modlist
 7   from lib389._mapped_object import DSLdapObject, DSLdapObjects
 8   from lib389.config import Config
 9 - from lib389.idm.account import Account, Accounts
10 + from lib389.idm.account import Account
11   from lib389.idm.nscontainer import nsContainers, nsContainer
12   from lib389.cos import CosPointerDefinitions, CosPointerDefinition, CosTemplates
13 - from lib389.utils import ensure_str, ensure_list_str, ensure_bytes
14   
15   USER_POLICY = 1
16   SUBTREE_POLICY = 2
17 @@ -146,6 +143,9 @@
18           # Add policy to the entry
19           user_entry.replace('pwdpolicysubentry', pwp_entry.dn)
20   
21 +         # make sure that local policies are enabled
22 +         self.set_global_policy({'nsslapd-pwpolicy-local': 'on'})
23 + 
24           return pwp_entry
25   
26       def create_subtree_policy(self, dn, properties):
27 @@ -187,6 +187,9 @@
28                                               'cosTemplateDn': cos_template.dn,
29                                               'cn': 'nsPwPolicy_CoS'})
30   
31 +         # make sure that local policies are enabled
32 +         self.set_global_policy({'nsslapd-pwpolicy-local': 'on'})
33 + 
34           return pwp_entry
35   
36       def get_pwpolicy_entry(self, dn):
  1 @@ -7,7 +7,6 @@
  2   # --- END COPYRIGHT BLOCK ---
  3   
  4   import ldap
  5 - import os
  6   import decimal
  7   import time
  8   import logging
  9 @@ -17,8 +16,6 @@
 10   from lib389._constants import *
 11   from lib389.properties import *
 12   from lib389.utils import normalizeDN, escapeDNValue, ensure_bytes, ensure_str, ensure_list_str, ds_is_older
 13 - from lib389._replication import RUV
 14 - from lib389.repltools import ReplTools
 15   from lib389 import DirSrv, Entry, NoSuchEntryError, InvalidArgumentError
 16   from lib389._mapped_object import DSLdapObjects, DSLdapObject
 17   from lib389.passwd import password_generate
 18 @@ -27,13 +24,10 @@
 19   from lib389.changelog import Changelog5
 20   
 21   from lib389.idm.domain import Domain
 22 - 
 23   from lib389.idm.group import Groups
 24   from lib389.idm.services import ServiceAccounts
 25   from lib389.idm.organizationalunit import OrganizationalUnits
 26   
 27 - from lib389.agreement import Agreements
 28 - 
 29   
 30   class ReplicaLegacy(object):
 31       proxied_methods = 'search_s getEntry'.split()
 32 @@ -883,6 +877,7 @@
 33                   return False
 34           return True
 35   
 36 + 
 37   class Replica(DSLdapObject):
 38       """Replica DSLdapObject with:
 39       - must attributes = ['cn', 'nsDS5ReplicaType', 'nsDS5ReplicaRoot',
 40 @@ -987,29 +982,38 @@
 41       def _delete_agreements(self):
 42           """Delete all the agreements for the suffix
 43   
 44 -         :raises: LDAPError - If failing to delete or search for agreeme        :type binddn: strnts
 45 +         :raises: LDAPError - If failing to delete or search for agreements
 46           """
 47           # Get the suffix
 48           self._populate_suffix()
 49 + 
 50 +         # Delete standard agmts
 51           agmts = self.get_agreements()
 52           for agmt in agmts.list():
 53               agmt.delete()
 54   
 55 -     def promote(self, newrole, binddn=None, rid=None):
 56 +         # Delete winysnc agmts
 57 +         agmts = self.get_agreements(winsync=True)
 58 +         for agmt in agmts.list():
 59 +             agmt.delete()
 60 + 
 61 +     def promote(self, newrole, binddn=None, binddn_group=None, rid=None):
 62           """Promote the replica to hub or master
 63   
 64           :param newrole: The new replication role for the replica: MASTER and HUB
 65           :type newrole: ReplicaRole
 66           :param binddn: The replication bind dn - only applied to master
 67           :type binddn: str
 68 +         :param binddn_group: The replication bind dn group - only applied to master
 69 +         :type binddn: str
 70           :param rid: The replication ID, applies only to promotions to "master"
 71           :type rid: int
 72 - 
 73           :returns: None
 74           :raises: ValueError - If replica is not promoted
 75           """
 76   
 77 -         if not binddn:
 78 + 
 79 +         if binddn is None and binddn_group is None:
 80               binddn = defaultProperties[REPLICATION_BIND_DN]
 81   
 82           # Check the role type
 83 @@ -1025,8 +1029,14 @@
 84               rid = CONSUMER_REPLICAID
 85   
 86           # Create the changelog
 87 +         cl = Changelog5(self._instance)
 88           try:
 89 -             self._instance.changelog.create()
 90 +             cl.create(properties={
 91 +                 'cn': 'changelog5',
 92 +                 'nsslapd-changelogdir': self._instance.get_changelog_dir()
 93 +             })
 94 +         except ldap.ALREADY_EXISTS:
 95 +             pass
 96           except ldap.LDAPError as e:
 97               raise ValueError('Failed to create changelog: %s' % str(e))
 98   
 99 @@ -1044,7 +1054,10 @@
100   
101           # Set bind dn
102           try:
103 -             self.set(REPL_BINDDN, binddn)
104 +             if binddn:
105 +                 self.set(REPL_BINDDN, binddn)
106 +             else:
107 +                 self.set(REPL_BIND_GROUP, binddn_group)
108           except ldap.LDAPError as e:
109               raise ValueError('Failed to update replica: ' + str(e))
110   
111 @@ -1169,12 +1182,13 @@
112   
113           return True
114   
115 -     def get_agreements(self):
116 +     def get_agreements(self, winsync=False):
117           """Return the set of agreements related to this suffix replica
118 - 
119 +         :param: winsync: If True then return winsync replication agreements,
120 +                          otherwise return teh standard replication agreements.
121           :returns: Agreements object
122           """
123 -         return Agreements(self._instance, self.dn)
124 +         return Agreements(self._instance, self.dn, winsync=winsync)
125   
126       def get_rid(self):
127           """Return the current replicas RID for this suffix
128 @@ -1187,6 +1201,7 @@
129           """Return the in memory ruv of this replica suffix.
130   
131           :returns: RUV object
132 +         :raises: LDAPError
133           """
134           self._populate_suffix()
135   
136 @@ -1201,11 +1216,29 @@
137   
138           return RUV(data)
139   
140 +     def get_ruv_agmt_maxcsns(self):
141 +         """Return the in memory ruv of this replica suffix.
142 + 
143 +         :returns: RUV object
144 +         :raises: LDAPError
145 +         """
146 +         self._populate_suffix()
147 + 
148 +         ent = self._instance.search_ext_s(
149 +             base=self._suffix,
150 +             scope=ldap.SCOPE_SUBTREE,
151 +             filterstr='(&(nsuniqueid=ffffffff-ffffffff-ffffffff-ffffffff)(objectclass=nstombstone))',
152 +             attrlist=['nsds5agmtmaxcsn'],
153 +             serverctrls=self._server_controls, clientctrls=self._client_controls)[0]
154 + 
155 +         return ensure_list_str(ent.getValues('nsds5agmtmaxcsn'))
156 + 
157       def begin_task_cl2ldif(self):
158           """Begin the changelog to ldif task
159           """
160           self.replace('nsds5task', 'cl2ldif')
161   
162 + 
163   class Replicas(DSLdapObjects):
164       """Replica DSLdapObjects for all replicas
165   
166 @@ -1239,6 +1272,7 @@
167               replica._populate_suffix()
168           return replica
169   
170 + 
171   class BootstrapReplicationManager(DSLdapObject):
172       """A Replication Manager credential for bootstrapping the repl process.
173       This is used by the replication manager object to coordinate the initial
174 @@ -1255,7 +1289,8 @@
175           self._must_attributes = ['cn', 'userPassword']
176           self._create_objectclasses = [
177               'top',
178 -             'netscapeServer'
179 +             'netscapeServer',
180 +             'nsAccount'
181               ]
182           self._protected = False
183           self.common_name = 'replication manager'