andykimpe / rpms / 389-ds-base

Forked from rpms/389-ds-base 5 months ago
Clone

Blame SOURCES/0084-Ticket-49576-Update-ds-replcheck-for-new-conflict-en.patch

3b7e51
From 19945c4807f6b3269fb65100ddaea5f596f68e72 Mon Sep 17 00:00:00 2001
3b7e51
From: Mark Reynolds <mreynolds@redhat.com>
3b7e51
Date: Fri, 18 May 2018 07:29:11 -0400
3b7e51
Subject: [PATCH 1/6] Ticket 49576 - Update ds-replcheck for new conflict
3b7e51
 entries
3b7e51
3b7e51
Description:  This patch addresses the recvent changes to conflict
3b7e51
              entries and tombstones.
3b7e51
3b7e51
https://pagure.io/389-ds-base/issue/49576
3b7e51
3b7e51
Reviewed by: tbordaz(Thanks!)
3b7e51
3b7e51
(cherry picked from commit 53e58cdbfb2a2672ac21cd9b6d59f8b345478324)
3b7e51
---
3b7e51
 ldap/admin/src/scripts/ds-replcheck | 456 +++++++++++++++++++---------
3b7e51
 1 file changed, 312 insertions(+), 144 deletions(-)
3b7e51
3b7e51
diff --git a/ldap/admin/src/scripts/ds-replcheck b/ldap/admin/src/scripts/ds-replcheck
3b7e51
index 45c4670a3..b801ccaa8 100755
3b7e51
--- a/ldap/admin/src/scripts/ds-replcheck
3b7e51
+++ b/ldap/admin/src/scripts/ds-replcheck
3b7e51
@@ -1,7 +1,7 @@
3b7e51
 #!/usr/bin/python
3b7e51
 
3b7e51
 # --- BEGIN COPYRIGHT BLOCK ---
3b7e51
-# Copyright (C) 2017 Red Hat, Inc.
3b7e51
+# Copyright (C) 2018 Red Hat, Inc.
3b7e51
 # All rights reserved.
3b7e51
 #
3b7e51
 # License: GPL (version 3 or any later version).
3b7e51
@@ -9,6 +9,7 @@
3b7e51
 # --- END COPYRIGHT BLOCK ---
3b7e51
 #
3b7e51
 
3b7e51
+import os
3b7e51
 import re
3b7e51
 import time
3b7e51
 import ldap
3b7e51
@@ -20,7 +21,7 @@ from ldap.ldapobject import SimpleLDAPObject
3b7e51
 from ldap.cidict import cidict
3b7e51
 from ldap.controls import SimplePagedResultsControl
3b7e51
 
3b7e51
-VERSION = "1.2"
3b7e51
+VERSION = "1.3"
3b7e51
 RUV_FILTER = '(&(nsuniqueid=ffffffff-ffffffff-ffffffff-ffffffff)(objectclass=nstombstone))'
3b7e51
 LDAP = 'ldap'
3b7e51
 LDAPS = 'ldaps'
3b7e51
@@ -36,6 +37,7 @@ class Entry(object):
3b7e51
     ''' This is a stripped down version of Entry from python-lib389.
3b7e51
     Once python-lib389 is released on RHEL this class will go away.
3b7e51
     '''
3b7e51
+
3b7e51
     def __init__(self, entrydata):
3b7e51
         if entrydata:
3b7e51
             self.dn = entrydata[0]
3b7e51
@@ -51,7 +53,7 @@ class Entry(object):
3b7e51
 
3b7e51
 
3b7e51
 def get_entry(entries, dn):
3b7e51
-    ''' Loop over enties looking for a matching dn
3b7e51
+    ''' Loop over a list of enties looking for a matching dn
3b7e51
     '''
3b7e51
     for entry in entries:
3b7e51
         if entry.dn == dn:
3b7e51
@@ -60,7 +62,7 @@ def get_entry(entries, dn):
3b7e51
 
3b7e51
 
3b7e51
 def remove_entry(rentries, dn):
3b7e51
-    ''' Remove an entry from the array of entries
3b7e51
+    ''' Remove an entry from the list of entries
3b7e51
     '''
3b7e51
     for entry in rentries:
3b7e51
         if entry.dn == dn:
3b7e51
@@ -69,7 +71,7 @@ def remove_entry(rentries, dn):
3b7e51
 
3b7e51
 
3b7e51
 def extract_time(stateinfo):
3b7e51
-    ''' Take the nscpEntryWSI attribute and get the most recent timestamp from
3b7e51
+    ''' Take the nscpEntryWSI(state info) attribute and get the most recent timestamp from
3b7e51
     one of the csns (vucsn, vdcsn, mdcsn, adcsn)
3b7e51
 
3b7e51
     Return the timestamp in decimal
3b7e51
@@ -87,7 +89,7 @@ def extract_time(stateinfo):
3b7e51
 
3b7e51
 
3b7e51
 def convert_timestamp(timestamp):
3b7e51
-    ''' Convert createtimestamp to ctime: 20170405184656Z -> Wed Apr  5 19:46:56 2017
3b7e51
+    ''' Convert createtimestamp to ctime: 20170405184656Z ----> Wed Apr  5 19:46:56 2017
3b7e51
     '''
3b7e51
     time_tuple = (int(timestamp[:4]), int(timestamp[4:6]), int(timestamp[6:8]),
3b7e51
                   int(timestamp[8:10]), int(timestamp[10:12]), int(timestamp[12:14]),
3b7e51
@@ -97,27 +99,43 @@ def convert_timestamp(timestamp):
3b7e51
 
3b7e51
 
3b7e51
 def convert_entries(entries):
3b7e51
-    '''Convert and normalize the ldap entries.  Take note of conflicts and tombstones
3b7e51
-    '''
3b7e51
+    '''For online report.  Convert and normalize the ldap entries.  Take note of
3b7e51
+    conflicts and tombstones '''
3b7e51
     new_entries = []
3b7e51
     conflict_entries = []
3b7e51
+    glue_entries = []
3b7e51
     result = {}
3b7e51
     tombstones = 0
3b7e51
+
3b7e51
     for entry in entries:
3b7e51
         new_entry = Entry(entry)
3b7e51
         new_entry.data = {k.lower(): v for k, v in list(new_entry.data.items())}
3b7e51
-        if 'nsds5replconflict' in new_entry.data:
3b7e51
+        if new_entry.dn.endswith("cn=mapping tree,cn=config"):
3b7e51
+            '''Skip replica entry (ldapsearch brings this in because the filter
3b7e51
+            we use triggers an internal operation to return the config entry - so
3b7e51
+            it must be skipped
3b7e51
+            '''
3b7e51
+            continue
3b7e51
+        if ('nsds5replconflict' in new_entry.data and 'nsTombstone' not in new_entry.data['objectclass'] and
3b7e51
+            'nstombstone' not in new_entry.data['objectclass']):
3b7e51
+            # This is a conflict entry that is NOT a tombstone entry (should this be reconsidered?)
3b7e51
             conflict_entries.append(new_entry)
3b7e51
+            if 'glue' in new_entry.data['objectclass']:
3b7e51
+                # A glue entry here is not necessarily a glue entry there.  Keep track of
3b7e51
+                # them for when we check missing entries
3b7e51
+                glue_entries.append(new_entry)
3b7e51
         else:
3b7e51
             new_entries.append(new_entry)
3b7e51
 
3b7e51
         if 'nstombstonecsn' in new_entry.data:
3b7e51
+            # Maintain tombstone count
3b7e51
             tombstones += 1
3b7e51
     del entries
3b7e51
 
3b7e51
     result['entries'] = new_entries
3b7e51
     result['conflicts'] = conflict_entries
3b7e51
     result['tombstones'] = tombstones
3b7e51
+    result['glue'] = glue_entries
3b7e51
 
3b7e51
     return result
3b7e51
 
3b7e51
@@ -174,20 +192,60 @@ def get_ruv_report(opts):
3b7e51
     return report
3b7e51
 
3b7e51
 
3b7e51
+def remove_attr_state_info(attr):
3b7e51
+    state_attr = None
3b7e51
+    idx = attr.find(';')
3b7e51
+    if idx > 0:
3b7e51
+        state_attr = attr  # preserve state info for diff report
3b7e51
+        if ";deleted" in attr:
3b7e51
+            # Ignore this attribute it was deleted
3b7e51
+            return None, state_attr
3b7e51
+        attr = attr[:idx]
3b7e51
+
3b7e51
+    return attr.lower(), state_attr
3b7e51
+
3b7e51
+def add_attr_entry(entry, val, attr, state_attr):
3b7e51
+    ''' Offline mode (ldif comparision) Add the attr to the entry, and if there
3b7e51
+    is state info add nscpentrywsi attr - we need consistency with online mode
3b7e51
+    to make code simpler '''
3b7e51
+    if attr is not None:
3b7e51
+        if attr in entry:
3b7e51
+            entry[attr].append(val)
3b7e51
+        else:
3b7e51
+            entry[attr] = [val]
3b7e51
+
3b7e51
+    # Handle state info for diff report
3b7e51
+    if state_attr is not None:
3b7e51
+        state_attr = state_attr + ": " + val
3b7e51
+        if 'nscpentrywsi' in entry:
3b7e51
+            entry['nscpentrywsi'].append(state_attr)
3b7e51
+        else:
3b7e51
+            entry['nscpentrywsi'] = [state_attr]
3b7e51
+    val = ""
3b7e51
+
3b7e51
+
3b7e51
 #
3b7e51
 # Offline mode helper functions
3b7e51
 #
3b7e51
-def ldif_search(LDIF, dn, conflicts=False):
3b7e51
-    ''' Search ldif by DN
3b7e51
+def ldif_search(LDIF, dn):
3b7e51
+    ''' Offline mode -  Search ldif for a single DN.  We need to factor in that
3b7e51
+    DN's and attribute values can wrap lines and are identified by a leading
3b7e51
+    white space.  So we can't fully process an attribute until we get to the
3b7e51
+    next attribute.
3b7e51
     '''
3b7e51
     result = {}
3b7e51
     data = {}
3b7e51
     found_conflict = False
3b7e51
+    found_subentry = False
3b7e51
     found_part_dn = False
3b7e51
+    found_part_val = False
3b7e51
+    found_attr = False
3b7e51
+    found_tombstone = False
3b7e51
+    found_glue = False
3b7e51
     found = False
3b7e51
-    reset_line = False
3b7e51
     count = 0
3b7e51
-
3b7e51
+    ignore_list = ['conflictcsn', 'modifytimestamp', 'modifiersname']
3b7e51
+    val = ""
3b7e51
     result['entry'] = None
3b7e51
     result['conflict'] = None
3b7e51
     result['tombstone'] = False
3b7e51
@@ -195,54 +253,132 @@ def ldif_search(LDIF, dn, conflicts=False):
3b7e51
     for line in LDIF:
3b7e51
         count += 1
3b7e51
         line = line.rstrip()
3b7e51
-        if reset_line:
3b7e51
-            reset_line = False
3b7e51
-            line = prev_line
3b7e51
+
3b7e51
         if found:
3b7e51
+            # We found our entry, now build up the entry (account from line wrap)
3b7e51
             if line == "":
3b7e51
-                # End of entry
3b7e51
+                # End of entry - update entry's last attribute value and break out
3b7e51
+                add_attr_entry(data, val, attr, state_attr)
3b7e51
+                val = ""
3b7e51
+                # Done!
3b7e51
                 break
3b7e51
 
3b7e51
             if line[0] == ' ':
3b7e51
-                # continuation line
3b7e51
-                prev = data[attr][len(data[attr]) - 1]
3b7e51
-                data[attr][len(data[attr]) - 1] = prev + line.strip()
3b7e51
+                # continuation line (wrapped value)
3b7e51
+                val += line[1:]
3b7e51
+                found_part_val = True
3b7e51
                 continue
3b7e51
+            elif found_part_val:
3b7e51
+                # We have the complete value now (it was wrapped)
3b7e51
+                found_part_val = False
3b7e51
+                found_attr = False
3b7e51
+                add_attr_entry(data, val, attr, state_attr)
3b7e51
+
3b7e51
+                # Now that the value is added to the entry lets process the new attribute...
3b7e51
+                value_set = line.split(":", 1)
3b7e51
+                attr, state_attr = remove_attr_state_info(value_set[0])
3b7e51
+
3b7e51
+                if attr in ignore_list or (attr is None and state_attr is None):
3b7e51
+                    # Skip it
3b7e51
+                    found_attr = False
3b7e51
+                    attr = None
3b7e51
+                    continue
3b7e51
 
3b7e51
-            value_set = line.split(":", 1)
3b7e51
-            attr = value_set[0].lower()
3b7e51
-            if attr.startswith('nsds5replconflict'):
3b7e51
-                found_conflict = True
3b7e51
-            if attr.startswith('nstombstonecsn'):
3b7e51
-                result['tombstone'] = True
3b7e51
-
3b7e51
-            if attr in data:
3b7e51
-                data[attr].append(value_set[1].strip())
3b7e51
+                val = value_set[1].strip()
3b7e51
+                found_attr = True
3b7e51
+
3b7e51
+                if attr is not None:
3b7e51
+                    # Set the entry type flags
3b7e51
+                    if attr.startswith('nsds5replconflict'):
3b7e51
+                        found_conflict = True
3b7e51
+                    if attr.startswith("objectclass") and val == "ldapsubentry":
3b7e51
+                        found_subentry = True
3b7e51
+                    if attr.startswith('nstombstonecsn'):
3b7e51
+                        result['tombstone'] = True
3b7e51
+                        found_tombstone = True
3b7e51
+                continue
3b7e51
             else:
3b7e51
-                data[attr] = [value_set[1].strip()]
3b7e51
+                # New attribute...
3b7e51
+                if found_attr:
3b7e51
+                    # But first we have to add the previous complete attr value to the entry data
3b7e51
+                    add_attr_entry(data, val, attr, state_attr)
3b7e51
+
3b7e51
+                # Process new attribute
3b7e51
+                value_set = line.split(":", 1)
3b7e51
+                attr, state_attr = remove_attr_state_info(value_set[0])
3b7e51
+                if attr is None or attr in ignore_list:
3b7e51
+                    # Skip it (its deleted)
3b7e51
+                    found_attr = False
3b7e51
+                    attr = None
3b7e51
+                    continue
3b7e51
+
3b7e51
+                val = value_set[1].strip()
3b7e51
+                found_attr = True
3b7e51
+
3b7e51
+                # Set the entry type flags
3b7e51
+                if attr.startswith('nsds5replconflict'):
3b7e51
+                    found_conflict = True
3b7e51
+                if attr.startswith("objectclass") and (val == "ldapsubentry" or val == "glue"):
3b7e51
+                    if val == "glue":
3b7e51
+                        found_glue = True
3b7e51
+                    found_subentry = True
3b7e51
+                if attr.startswith('nstombstonecsn'):
3b7e51
+                    result['tombstone'] = True
3b7e51
+                    found_tombstone = True
3b7e51
+                continue
3b7e51
+
3b7e51
         elif found_part_dn:
3b7e51
             if line[0] == ' ':
3b7e51
+                # DN is still wrapping, keep building up the dn value
3b7e51
                 part_dn += line[1:].lower()
3b7e51
             else:
3b7e51
-                # We have the full dn
3b7e51
+                # We now have the full dn
3b7e51
                 found_part_dn = False
3b7e51
-                reset_line = True
3b7e51
-                prev_line = line
3b7e51
                 if part_dn == dn:
3b7e51
+                    # We found our entry
3b7e51
                     found = True
3b7e51
+
3b7e51
+                    # But now we have a new attribute to process
3b7e51
+                    value_set = line.split(":", 1)
3b7e51
+                    attr, state_attr = remove_attr_state_info(value_set[0])
3b7e51
+                    if attr is None or attr in ignore_list:
3b7e51
+                        # Skip it (its deleted)
3b7e51
+                        found_attr = False
3b7e51
+                        attr = None
3b7e51
+                        continue
3b7e51
+
3b7e51
+                    val = value_set[1].strip()
3b7e51
+                    found_attr = True
3b7e51
+
3b7e51
+                    if attr.startswith('nsds5replconflict'):
3b7e51
+                        found_conflict = True
3b7e51
+                    if attr.startswith("objectclass") and val == "ldapsubentry":
3b7e51
+                        found_subentry = True
3b7e51
+
3b7e51
+                    if attr.startswith('nstombstonecsn'):
3b7e51
+                        result['tombstone'] = True
3b7e51
+                        found_tombstone = True
3b7e51
                     continue
3b7e51
+
3b7e51
         if line.startswith('dn: '):
3b7e51
             if line[4:].lower() == dn:
3b7e51
+                # We got our full DN, now process the entry
3b7e51
                 found = True
3b7e51
                 continue
3b7e51
             else:
3b7e51
+                # DN wraps the line, keep looping until we get the whole value
3b7e51
                 part_dn = line[4:].lower()
3b7e51
                 found_part_dn = True
3b7e51
 
3b7e51
+    # Keep track of entry index - we use this later when searching the LDIF again
3b7e51
     result['idx'] = count
3b7e51
-    if found_conflict:
3b7e51
+
3b7e51
+    result['glue'] = None
3b7e51
+    if found_conflict and found_subentry and found_tombstone is False:
3b7e51
         result['entry'] = None
3b7e51
         result['conflict'] = Entry([dn, data])
3b7e51
+        if found_glue:
3b7e51
+            result['glue'] = result['conflict']
3b7e51
     elif found:
3b7e51
         result['conflict'] = None
3b7e51
         result['entry'] = Entry([dn, data])
3b7e51
@@ -251,7 +387,7 @@ def ldif_search(LDIF, dn, conflicts=False):
3b7e51
 
3b7e51
 
3b7e51
 def get_dns(LDIF, opts):
3b7e51
-    ''' Get all the DN's
3b7e51
+    ''' Get all the DN's from an LDIF file
3b7e51
     '''
3b7e51
     dns = []
3b7e51
     found = False
3b7e51
@@ -275,7 +411,7 @@ def get_dns(LDIF, opts):
3b7e51
 
3b7e51
 
3b7e51
 def get_ldif_ruv(LDIF, opts):
3b7e51
-    ''' Search the ldif and get the ruv entry
3b7e51
+    ''' Search the LDIF and get the ruv entry
3b7e51
     '''
3b7e51
     LDIF.seek(0)
3b7e51
     result = ldif_search(LDIF, opts['ruv_dn'])
3b7e51
@@ -283,7 +419,7 @@ def get_ldif_ruv(LDIF, opts):
3b7e51
 
3b7e51
 
3b7e51
 def cmp_entry(mentry, rentry, opts):
3b7e51
-    ''' Compare the two entries, and return a diff map
3b7e51
+    ''' Compare the two entries, and return a "diff map"
3b7e51
     '''
3b7e51
     diff = {}
3b7e51
     diff['dn'] = mentry['dn']
3b7e51
@@ -307,6 +443,7 @@ def cmp_entry(mentry, rentry, opts):
3b7e51
                 diff['missing'].append(" - Replica missing attribute: \"%s\"" % (mattr))
3b7e51
                 diff_count += 1
3b7e51
                 if 'nscpentrywsi' in mentry.data:
3b7e51
+                    # Great we have state info so we can provide details about the missing attribute
3b7e51
                     found = False
3b7e51
                     for val in mentry.data['nscpentrywsi']:
3b7e51
                         if val.lower().startswith(mattr + ';'):
3b7e51
@@ -316,6 +453,7 @@ def cmp_entry(mentry, rentry, opts):
3b7e51
                             diff['missing'].append(" - Master's State Info: %s" % (val))
3b7e51
                             diff['missing'].append(" - Date: %s\n" % (time.ctime(extract_time(val))))
3b7e51
                 else:
3b7e51
+                    # No state info, just move on
3b7e51
                     diff['missing'].append("")
3b7e51
 
3b7e51
         elif mentry.data[mattr] != rentry.data[mattr]:
3b7e51
@@ -335,6 +473,9 @@ def cmp_entry(mentry, rentry, opts):
3b7e51
                     if not found:
3b7e51
                         diff['diff'].append("      Master: ")
3b7e51
                         for val in mentry.data[mattr]:
3b7e51
+                            # This is an "origin" value which means it's never been
3b7e51
+                            # updated since replication was set up.  So its the
3b7e51
+                            # original value
3b7e51
                             diff['diff'].append("        - Origin value: %s" % (val))
3b7e51
                         diff['diff'].append("")
3b7e51
 
3b7e51
@@ -350,10 +491,13 @@ def cmp_entry(mentry, rentry, opts):
3b7e51
                     if not found:
3b7e51
                         diff['diff'].append("      Replica: ")
3b7e51
                         for val in rentry.data[mattr]:
3b7e51
+                            # This is an "origin" value which means it's never been
3b7e51
+                            # updated since replication was set up.  So its the
3b7e51
+                            # original value
3b7e51
                             diff['diff'].append("        - Origin value: %s" % (val))
3b7e51
                         diff['diff'].append("")
3b7e51
                 else:
3b7e51
-                    # no state info
3b7e51
+                    # no state info, report what we got
3b7e51
                     diff['diff'].append("      Master: ")
3b7e51
                     for val in mentry.data[mattr]:
3b7e51
                         diff['diff'].append("        - %s: %s" % (mattr, val))
3b7e51
@@ -436,40 +580,62 @@ def do_offline_report(opts, output_file=None):
3b7e51
     MLDIF.seek(idx)
3b7e51
     RLDIF.seek(idx)
3b7e51
 
3b7e51
-    # Compare the master entries with the replica's
3b7e51
+    """ Compare the master entries with the replica's.  Take our list of dn's from
3b7e51
+    the master ldif and get that entry( dn) from the master and replica ldif.  In
3b7e51
+    this phase we keep keep track of conflict/tombstone counts, and we check for
3b7e51
+    missing entries and entry differences.   We only need to do the entry diff
3b7e51
+    checking in this phase - we do not need to do it when process the replica dn's
3b7e51
+    because if the entry exists in both LDIF's then we already checked or diffs
3b7e51
+    while processing the master dn's.
3b7e51
+    """
3b7e51
     print ("Comparing Master to Replica...")
3b7e51
     missing = False
3b7e51
     for dn in master_dns:
3b7e51
-        mresult = ldif_search(MLDIF, dn, True)
3b7e51
-        rresult = ldif_search(RLDIF, dn, True)
3b7e51
+        mresult = ldif_search(MLDIF, dn)
3b7e51
+        rresult = ldif_search(RLDIF, dn)
3b7e51
+
3b7e51
+        if dn in replica_dns:
3b7e51
+            if (rresult['entry'] is not None or rresult['glue'] is not None or
3b7e51
+                rresult['conflict'] is not None or rresult['tombstone']):
3b7e51
+                """ We can safely remove this DN from the replica dn list as it
3b7e51
+                does not need to be checked again.  This also speeds things up
3b7e51
+                when doing the replica vs master phase.
3b7e51
+                """
3b7e51
+                replica_dns.remove(dn)
3b7e51
 
3b7e51
         if mresult['tombstone']:
3b7e51
             mtombstones += 1
3b7e51
+            # continue
3b7e51
+        if rresult['tombstone']:
3b7e51
+            rtombstones += 1
3b7e51
 
3b7e51
         if mresult['conflict'] is not None or rresult['conflict'] is not None:
3b7e51
+            # If either entry is a conflict we still process it here
3b7e51
             if mresult['conflict'] is not None:
3b7e51
                 mconflicts.append(mresult['conflict'])
3b7e51
+            if rresult['conflict'] is not None:
3b7e51
+                rconflicts.append(rresult['conflict'])
3b7e51
         elif rresult['entry'] is None:
3b7e51
-            # missing entry - restart the search from beginning
3b7e51
+            # missing entry - restart the search from beginning in case it got skipped
3b7e51
             RLDIF.seek(0)
3b7e51
             rresult = ldif_search(RLDIF, dn)
3b7e51
-            if rresult['entry'] is None:
3b7e51
-                # missing entry in rentries
3b7e51
-                RLDIF.seek(mresult['idx'])  # Set the cursor to the last good line
3b7e51
+            if rresult['entry'] is None and rresult['glue'] is None:
3b7e51
+                # missing entry in Replica(rentries)
3b7e51
+                RLDIF.seek(mresult['idx'])  # Set the LDIF cursor/index to the last good line
3b7e51
                 if not missing:
3b7e51
-                    missing_report += ('Replica is missing entries:\n')
3b7e51
+                    missing_report += ('  Entries missing on Replica:\n')
3b7e51
                     missing = True
3b7e51
                 if mresult['entry'] and 'createtimestamp' in mresult['entry'].data:
3b7e51
-                    missing_report += ('  - %s  (Master\'s creation date:  %s)\n' %
3b7e51
+                    missing_report += ('   - %s  (Created on Master at: %s)\n' %
3b7e51
                                        (dn, convert_timestamp(mresult['entry'].data['createtimestamp'][0])))
3b7e51
                 else:
3b7e51
                     missing_report += ('  - %s\n' % dn)
3b7e51
-            else:
3b7e51
+            elif mresult['tombstone'] is False:
3b7e51
                 # Compare the entries
3b7e51
                 diff = cmp_entry(mresult['entry'], rresult['entry'], opts)
3b7e51
                 if diff:
3b7e51
                     diff_report.append(format_diff(diff))
3b7e51
-        else:
3b7e51
+        elif mresult['tombstone'] is False:
3b7e51
             # Compare the entries
3b7e51
             diff = cmp_entry(mresult['entry'], rresult['entry'], opts)
3b7e51
             if diff:
3b7e51
@@ -478,7 +644,10 @@ def do_offline_report(opts, output_file=None):
3b7e51
     if missing:
3b7e51
         missing_report += ('\n')
3b7e51
 
3b7e51
-    # Search Replica, and look for missing entries only.  Count entries as well
3b7e51
+    """ Search Replica, and look for missing entries only.  We already did the
3b7e51
+    diff checking, so its only missing entries we are worried about. Count the
3b7e51
+    remaining conflict & tombstone entries as well.
3b7e51
+    """
3b7e51
     print ("Comparing Replica to Master...")
3b7e51
     MLDIF.seek(0)
3b7e51
     RLDIF.seek(0)
3b7e51
@@ -486,26 +655,26 @@ def do_offline_report(opts, output_file=None):
3b7e51
     for dn in replica_dns:
3b7e51
         rresult = ldif_search(RLDIF, dn)
3b7e51
         mresult = ldif_search(MLDIF, dn)
3b7e51
-
3b7e51
         if rresult['tombstone']:
3b7e51
             rtombstones += 1
3b7e51
-        if mresult['entry'] is not None or rresult['conflict'] is not None:
3b7e51
-            if rresult['conflict'] is not None:
3b7e51
-                rconflicts.append(rresult['conflict'])
3b7e51
+            # continue
3b7e51
+
3b7e51
+        if rresult['conflict'] is not None:
3b7e51
+            rconflicts.append(rresult['conflict'])
3b7e51
         elif mresult['entry'] is None:
3b7e51
             # missing entry
3b7e51
             MLDIF.seek(0)
3b7e51
             mresult = ldif_search(MLDIF, dn)
3b7e51
-            if mresult['entry'] is None and mresult['conflict'] is not None:
3b7e51
-                MLDIF.seek(rresult['idx'])  # Set the cursor to the last good line
3b7e51
+            if mresult['entry'] is None and mresult['glue'] is None:
3b7e51
+                MLDIF.seek(rresult['idx'])  # Set the LDIF cursor/index to the last good line
3b7e51
                 if not missing:
3b7e51
-                    missing_report += ('Master is missing entries:\n')
3b7e51
+                    missing_report += ('  Entries missing on Master:\n')
3b7e51
                     missing = True
3b7e51
-                if 'createtimestamp' in rresult['entry'].data:
3b7e51
-                    missing_report += ('  - %s  (Replica\'s creation date:  %s)\n' %
3b7e51
+                if rresult['entry'] and 'createtimestamp' in rresult['entry'].data:
3b7e51
+                    missing_report += ('   - %s  (Created on Replica at: %s)\n' %
3b7e51
                                        (dn, convert_timestamp(rresult['entry'].data['createtimestamp'][0])))
3b7e51
                 else:
3b7e51
-                    missing_report += ('  - %s\n')
3b7e51
+                    missing_report += ('  - %s\n' % dn)
3b7e51
     if missing:
3b7e51
         missing_report += ('\n')
3b7e51
 
3b7e51
@@ -553,8 +722,8 @@ def do_offline_report(opts, output_file=None):
3b7e51
         print(final_report)
3b7e51
 
3b7e51
 
3b7e51
-def check_for_diffs(mentries, rentries, report, opts):
3b7e51
-    ''' Check for diffs, return the updated report
3b7e51
+def check_for_diffs(mentries, mglue, rentries, rglue, report, opts):
3b7e51
+    ''' Online mode only - Check for diffs, return the updated report
3b7e51
     '''
3b7e51
     diff_report = []
3b7e51
     m_missing = []
3b7e51
@@ -569,18 +738,26 @@ def check_for_diffs(mentries, rentries, report, opts):
3b7e51
     for mentry in mentries:
3b7e51
         rentry = get_entry(rentries, mentry.dn)
3b7e51
         if rentry:
3b7e51
-            diff = cmp_entry(mentry, rentry, opts)
3b7e51
-            if diff:
3b7e51
-                diff_report.append(format_diff(diff))
3b7e51
+            if 'nsTombstone' not in rentry.data['objectclass'] and 'nstombstone' not in rentry.data['objectclass']:
3b7e51
+                diff = cmp_entry(mentry, rentry, opts)
3b7e51
+                if diff:
3b7e51
+                    diff_report.append(format_diff(diff))
3b7e51
             # Now remove the rentry from the rentries so we can find stragglers
3b7e51
             remove_entry(rentries, rentry.dn)
3b7e51
         else:
3b7e51
-            # Add missing entry in Replica
3b7e51
-            r_missing.append(mentry)
3b7e51
+            rentry = get_entry(rglue, mentry.dn)
3b7e51
+            if rentry:
3b7e51
+                # Glue entry nothing to compare
3b7e51
+                remove_entry(rentries, rentry.dn)
3b7e51
+            else:
3b7e51
+                # Add missing entry in Replica
3b7e51
+                r_missing.append(mentry)
3b7e51
 
3b7e51
     for rentry in rentries:
3b7e51
         # We should not have any entries if we are sync
3b7e51
-        m_missing.append(rentry)
3b7e51
+        mentry = get_entry(mglue, rentry.dn)
3b7e51
+        if mentry is None:
3b7e51
+            m_missing.append(rentry)
3b7e51
 
3b7e51
     if len(diff_report) > 0:
3b7e51
         report['diff'] += diff_report
3b7e51
@@ -609,6 +786,12 @@ def connect_to_replicas(opts):
3b7e51
         ruri = "%s://%s:%s/" % (opts['rprotocol'], opts['rhost'], opts['rport'])
3b7e51
     replica = SimpleLDAPObject(ruri)
3b7e51
 
3b7e51
+    # Set timeouts
3b7e51
+    master.set_option(ldap.OPT_NETWORK_TIMEOUT,5.0)
3b7e51
+    master.set_option(ldap.OPT_TIMEOUT,5.0)
3b7e51
+    replica.set_option(ldap.OPT_NETWORK_TIMEOUT,5.0)
3b7e51
+    replica.set_option(ldap.OPT_TIMEOUT,5.0)
3b7e51
+
3b7e51
     # Setup Secure Conenction
3b7e51
     if opts['certdir'] is not None:
3b7e51
         # Setup Master
3b7e51
@@ -620,7 +803,7 @@ def connect_to_replicas(opts):
3b7e51
                 try:
3b7e51
                     master.start_tls_s()
3b7e51
                 except ldap.LDAPError as e:
3b7e51
-                    print('TLS negotiation failed on Master: %s' % str(e))
3b7e51
+                    print('TLS negotiation failed on Master: {}'.format(str(e)))
3b7e51
                     exit(1)
3b7e51
 
3b7e51
         # Setup Replica
3b7e51
@@ -632,7 +815,7 @@ def connect_to_replicas(opts):
3b7e51
                 try:
3b7e51
                     replica.start_tls_s()
3b7e51
                 except ldap.LDAPError as e:
3b7e51
-                    print('TLS negotiation failed on Master: %s' % str(e))
3b7e51
+                    print('TLS negotiation failed on Master: {}'.format(str(e)))
3b7e51
                     exit(1)
3b7e51
 
3b7e51
     # Open connection to master
3b7e51
@@ -642,7 +825,8 @@ def connect_to_replicas(opts):
3b7e51
         print("Cannot connect to %r" % muri)
3b7e51
         exit(1)
3b7e51
     except ldap.LDAPError as e:
3b7e51
-        print("Error: Failed to authenticate to Master: %s", str(e))
3b7e51
+        print("Error: Failed to authenticate to Master: ({}).  "
3b7e51
+              "Please check your credentials and LDAP urls are correct.".format(str(e)))
3b7e51
         exit(1)
3b7e51
 
3b7e51
     # Open connection to replica
3b7e51
@@ -652,7 +836,8 @@ def connect_to_replicas(opts):
3b7e51
         print("Cannot connect to %r" % ruri)
3b7e51
         exit(1)
3b7e51
     except ldap.LDAPError as e:
3b7e51
-        print("Error: Failed to authenticate to Replica: %s", str(e))
3b7e51
+        print("Error: Failed to authenticate to Replica: ({}).  "
3b7e51
+              "Please check your credentials and LDAP urls are correct.".format(str(e)))
3b7e51
         exit(1)
3b7e51
 
3b7e51
     # Get the RUVs
3b7e51
@@ -665,7 +850,7 @@ def connect_to_replicas(opts):
3b7e51
             print("Error: Master does not have an RUV entry")
3b7e51
             exit(1)
3b7e51
     except ldap.LDAPError as e:
3b7e51
-        print("Error: Failed to get Master RUV entry: %s", str(e))
3b7e51
+        print("Error: Failed to get Master RUV entry: {}".format(str(e)))
3b7e51
         exit(1)
3b7e51
 
3b7e51
     print ("Gathering Replica's RUV...")
3b7e51
@@ -678,7 +863,7 @@ def connect_to_replicas(opts):
3b7e51
             exit(1)
3b7e51
 
3b7e51
     except ldap.LDAPError as e:
3b7e51
-        print("Error: Failed to get Replica RUV entry: %s", str(e))
3b7e51
+        print("Error: Failed to get Replica RUV entry: {}".format(str(e)))
3b7e51
         exit(1)
3b7e51
 
3b7e51
     return (master, replica, opts)
3b7e51
@@ -687,6 +872,7 @@ def connect_to_replicas(opts):
3b7e51
 def print_online_report(report, opts, output_file):
3b7e51
     ''' Print the online report
3b7e51
     '''
3b7e51
+
3b7e51
     print ('Preparing final report...')
3b7e51
     m_missing = len(report['m_missing'])
3b7e51
     r_missing = len(report['r_missing'])
3b7e51
@@ -711,22 +897,23 @@ def print_online_report(report, opts, output_file):
3b7e51
         missing = True
3b7e51
         final_report += ('\nMissing Entries\n')
3b7e51
         final_report += ('=====================================================\n\n')
3b7e51
-        if m_missing > 0:
3b7e51
-            final_report += ('  Entries missing on Master:\n')
3b7e51
-            for entry in report['m_missing']:
3b7e51
+
3b7e51
+        if r_missing > 0:
3b7e51
+            final_report += ('  Entries missing on Replica:\n')
3b7e51
+            for entry in report['r_missing']:
3b7e51
                 if 'createtimestamp' in entry.data:
3b7e51
-                    final_report += ('   - %s  (Created on Replica at: %s)\n' %
3b7e51
+                    final_report += ('   - %s  (Created on Master at: %s)\n' %
3b7e51
                                      (entry.dn, convert_timestamp(entry.data['createtimestamp'][0])))
3b7e51
                 else:
3b7e51
                     final_report += ('   - %s\n' % (entry.dn))
3b7e51
 
3b7e51
-        if r_missing > 0:
3b7e51
-            if m_missing > 0:
3b7e51
+        if m_missing > 0:
3b7e51
+            if r_missing > 0:
3b7e51
                 final_report += ('\n')
3b7e51
-            final_report += ('  Entries missing on Replica:\n')
3b7e51
-            for entry in report['r_missing']:
3b7e51
+            final_report += ('  Entries missing on Master:\n')
3b7e51
+            for entry in report['m_missing']:
3b7e51
                 if 'createtimestamp' in entry.data:
3b7e51
-                    final_report += ('   - %s  (Created on Master at: %s)\n' %
3b7e51
+                    final_report += ('   - %s  (Created on Replica at: %s)\n' %
3b7e51
                                      (entry.dn, convert_timestamp(entry.data['createtimestamp'][0])))
3b7e51
                 else:
3b7e51
                     final_report += ('   - %s\n' % (entry.dn))
3b7e51
@@ -751,7 +938,8 @@ def print_online_report(report, opts, output_file):
3b7e51
 def remove_state_info(entry):
3b7e51
     ''' Remove the state info for the attributes used in the conflict report
3b7e51
     '''
3b7e51
-    attrs = ['objectclass', 'nsds5replconflict', 'createtimestamp']
3b7e51
+    attrs = ['objectclass', 'nsds5replconflict', 'createtimestamp' , 'modifytimestamp']
3b7e51
+    # attrs = ['createtimestamp']
3b7e51
     for key, val in list(entry.data.items()):
3b7e51
         for attr in attrs:
3b7e51
             if key.lower().startswith(attr):
3b7e51
@@ -766,9 +954,6 @@ def get_conflict_report(mentries, rentries, verbose, format_conflicts=False):
3b7e51
     r_conflicts = []
3b7e51
 
3b7e51
     for entry in mentries:
3b7e51
-        if format_conflicts:
3b7e51
-            remove_state_info(entry)
3b7e51
-
3b7e51
         if 'glue' in entry.data['objectclass']:
3b7e51
             m_conflicts.append({'dn': entry.dn, 'conflict': entry.data['nsds5replconflict'][0],
3b7e51
                                 'date': entry.data['createtimestamp'][0], 'glue': 'yes'})
3b7e51
@@ -776,9 +961,6 @@ def get_conflict_report(mentries, rentries, verbose, format_conflicts=False):
3b7e51
             m_conflicts.append({'dn': entry.dn, 'conflict': entry.data['nsds5replconflict'][0],
3b7e51
                                 'date': entry.data['createtimestamp'][0], 'glue': 'no'})
3b7e51
     for entry in rentries:
3b7e51
-        if format_conflicts:
3b7e51
-            remove_state_info(entry)
3b7e51
-
3b7e51
         if 'glue' in entry.data['objectclass']:
3b7e51
             r_conflicts.append({'dn': entry.dn, 'conflict': entry.data['nsds5replconflict'][0],
3b7e51
                                 'date': entry.data['createtimestamp'][0], 'glue': 'yes'})
3b7e51
@@ -790,7 +972,7 @@ def get_conflict_report(mentries, rentries, verbose, format_conflicts=False):
3b7e51
         report = "\n\nConflict Entries\n"
3b7e51
         report += "=====================================================\n\n"
3b7e51
         if len(m_conflicts) > 0:
3b7e51
-            report += ('Master Conflict Entries: %d\n' % (len(m_conflicts)))
3b7e51
+            report += ('Master Conflict Entries:  %d\n' % (len(m_conflicts)))
3b7e51
             if verbose:
3b7e51
                 for entry in m_conflicts:
3b7e51
                     report += ('\n - %s\n' % (entry['dn']))
3b7e51
@@ -799,7 +981,7 @@ def get_conflict_report(mentries, rentries, verbose, format_conflicts=False):
3b7e51
                     report += ('    - Created:    %s\n' % (convert_timestamp(entry['date'])))
3b7e51
 
3b7e51
         if len(r_conflicts) > 0:
3b7e51
-            if len(m_conflicts) > 0:
3b7e51
+            if len(m_conflicts) > 0 and verbose:
3b7e51
                 report += "\n"  # add spacer
3b7e51
             report += ('Replica Conflict Entries: %d\n' % (len(r_conflicts)))
3b7e51
             if verbose:
3b7e51
@@ -814,46 +996,6 @@ def get_conflict_report(mentries, rentries, verbose, format_conflicts=False):
3b7e51
         return ""
3b7e51
 
3b7e51
 
3b7e51
-def get_tombstones(replica, opts):
3b7e51
-    ''' Return the number of tombstones
3b7e51
-    '''
3b7e51
-    paged_ctrl = SimplePagedResultsControl(True, size=opts['pagesize'], cookie='')
3b7e51
-    controls = [paged_ctrl]
3b7e51
-    req_pr_ctrl = controls[0]
3b7e51
-    count = 0
3b7e51
-
3b7e51
-    try:
3b7e51
-        msgid = replica.search_ext(opts['suffix'], ldap.SCOPE_SUBTREE,
3b7e51
-                                   '(&(objectclass=nstombstone)(nstombstonecsn=*))',
3b7e51
-                                   ['dn'], serverctrls=controls)
3b7e51
-    except ldap.LDAPError as e:
3b7e51
-        print("Error: Failed to get tombstone entries: %s", str(e))
3b7e51
-        exit(1)
3b7e51
-
3b7e51
-    done = False
3b7e51
-    while not done:
3b7e51
-        rtype, rdata, rmsgid, rctrls = replica.result3(msgid)
3b7e51
-        count += len(rdata)
3b7e51
-
3b7e51
-        pctrls = [
3b7e51
-                c
3b7e51
-                for c in rctrls
3b7e51
-                if c.controlType == SimplePagedResultsControl.controlType
3b7e51
-                ]
3b7e51
-        if pctrls:
3b7e51
-            if pctrls[0].cookie:
3b7e51
-                # Copy cookie from response control to request control
3b7e51
-                req_pr_ctrl.cookie = pctrls[0].cookie
3b7e51
-                msgid = replica.search_ext(opts['suffix'], ldap.SCOPE_SUBTREE,
3b7e51
-                                           '(&(objectclass=nstombstone)(nstombstonecsn=*))',
3b7e51
-                                           ['dn'], serverctrls=controls)
3b7e51
-            else:
3b7e51
-                done = True  # No more pages available
3b7e51
-        else:
3b7e51
-            done = True
3b7e51
-    return count
3b7e51
-
3b7e51
-
3b7e51
 def do_online_report(opts, output_file=None):
3b7e51
     ''' Check for differences between two replicas
3b7e51
     '''
3b7e51
@@ -880,7 +1022,7 @@ def do_online_report(opts, output_file=None):
3b7e51
     req_pr_ctrl = controls[0]
3b7e51
     try:
3b7e51
         master_msgid = master.search_ext(opts['suffix'], ldap.SCOPE_SUBTREE,
3b7e51
-                                         "(|(objectclass=*)(objectclass=ldapsubentry))",
3b7e51
+                                         "(|(objectclass=*)(objectclass=ldapsubentry)(objectclass=nstombstone))",
3b7e51
                                          ['*', 'createtimestamp', 'nscpentrywsi', 'nsds5replconflict'],
3b7e51
                                          serverctrls=controls)
3b7e51
     except ldap.LDAPError as e:
3b7e51
@@ -888,7 +1030,7 @@ def do_online_report(opts, output_file=None):
3b7e51
         exit(1)
3b7e51
     try:
3b7e51
         replica_msgid = replica.search_ext(opts['suffix'], ldap.SCOPE_SUBTREE,
3b7e51
-                                           "(|(objectclass=*)(objectclass=ldapsubentry))",
3b7e51
+                                           "(|(objectclass=*)(objectclass=ldapsubentry)(objectclass=nstombstone))",
3b7e51
                                            ['*', 'createtimestamp', 'nscpentrywsi', 'nsds5replconflict'],
3b7e51
                                            serverctrls=controls)
3b7e51
     except ldap.LDAPError as e:
3b7e51
@@ -918,7 +1060,9 @@ def do_online_report(opts, output_file=None):
3b7e51
         rconflicts += rresult['conflicts']
3b7e51
 
3b7e51
         # Check for diffs
3b7e51
-        report = check_for_diffs(mresult['entries'], rresult['entries'], report, opts)
3b7e51
+        report = check_for_diffs(mresult['entries'], mresult['glue'],
3b7e51
+                                 rresult['entries'], rresult['glue'],
3b7e51
+                                 report, opts)
3b7e51
 
3b7e51
         if not m_done:
3b7e51
             # Master
3b7e51
@@ -933,7 +1077,7 @@ def do_online_report(opts, output_file=None):
3b7e51
                     req_pr_ctrl.cookie = m_pctrls[0].cookie
3b7e51
                     master_msgid = master.search_ext(opts['suffix'], ldap.SCOPE_SUBTREE,
3b7e51
                         "(|(objectclass=*)(objectclass=ldapsubentry))",
3b7e51
-                        ['*', 'createtimestamp', 'nscpentrywsi', 'nsds5replconflict'], serverctrls=controls)
3b7e51
+                        ['*', 'createtimestamp', 'nscpentrywsi', 'conflictcsn', 'nsds5replconflict'], serverctrls=controls)
3b7e51
                 else:
3b7e51
                     m_done = True  # No more pages available
3b7e51
             else:
3b7e51
@@ -953,7 +1097,7 @@ def do_online_report(opts, output_file=None):
3b7e51
                     req_pr_ctrl.cookie = r_pctrls[0].cookie
3b7e51
                     replica_msgid = replica.search_ext(opts['suffix'], ldap.SCOPE_SUBTREE,
3b7e51
                         "(|(objectclass=*)(objectclass=ldapsubentry))",
3b7e51
-                        ['*', 'createtimestamp', 'nscpentrywsi', 'nsds5replconflict'], serverctrls=controls)
3b7e51
+                        ['*', 'createtimestamp', 'nscpentrywsi', 'conflictcsn', 'nsds5replconflict'], serverctrls=controls)
3b7e51
                 else:
3b7e51
                     r_done = True  # No more pages available
3b7e51
             else:
3b7e51
@@ -961,10 +1105,8 @@ def do_online_report(opts, output_file=None):
3b7e51
 
3b7e51
     # Get conflicts & tombstones
3b7e51
     report['conflict'] = get_conflict_report(mconflicts, rconflicts, opts['conflicts'])
3b7e51
-    report['mtombstones'] = get_tombstones(master, opts)
3b7e51
-    report['rtombstones'] = get_tombstones(replica, opts)
3b7e51
-    report['m_count'] += report['mtombstones']
3b7e51
-    report['r_count'] += report['rtombstones']
3b7e51
+    report['mtombstones'] = mresult['tombstones']
3b7e51
+    report['rtombstones'] = rresult['tombstones']
3b7e51
 
3b7e51
     # Do the final report
3b7e51
     print_online_report(report, opts, output_file)
3b7e51
@@ -1027,11 +1169,16 @@ def main():
3b7e51
 
3b7e51
     # Parse the ldap URLs
3b7e51
     if args.murl is not None and args.rurl is not None:
3b7e51
+        # Make sure the URLs are different
3b7e51
+        if args.murl == args.rurl:
3b7e51
+            print("Master and Replica LDAP URLs are the same, they must be different")
3b7e51
+            exit(1)
3b7e51
+
3b7e51
         # Parse Master url
3b7e51
-        murl = ldapurl.LDAPUrl(args.murl)
3b7e51
         if not ldapurl.isLDAPUrl(args.murl):
3b7e51
             print("Master LDAP URL is invalid")
3b7e51
             exit(1)
3b7e51
+        murl = ldapurl.LDAPUrl(args.murl)
3b7e51
         if murl.urlscheme in VALID_PROTOCOLS:
3b7e51
             opts['mprotocol'] = murl.urlscheme
3b7e51
         else:
3b7e51
@@ -1052,10 +1199,10 @@ def main():
3b7e51
             opts['mport'] = parts[1]
3b7e51
 
3b7e51
         # Parse Replica url
3b7e51
-        rurl = ldapurl.LDAPUrl(args.rurl)
3b7e51
         if not ldapurl.isLDAPUrl(args.rurl):
3b7e51
             print("Replica LDAP URL is invalid")
3b7e51
             exit(1)
3b7e51
+        rurl = ldapurl.LDAPUrl(args.rurl)
3b7e51
         if rurl.urlscheme in VALID_PROTOCOLS:
3b7e51
             opts['rprotocol'] = rurl.urlscheme
3b7e51
         else:
3b7e51
@@ -1075,11 +1222,19 @@ def main():
3b7e51
             opts['rhost'] = parts[0]
3b7e51
             opts['rport'] = parts[1]
3b7e51
 
3b7e51
+    # Validate certdir
3b7e51
+    opts['certdir'] = None
3b7e51
+    if args.certdir:
3b7e51
+        if os.path.exists() and os.path.isdir(certdir):
3b7e51
+            opts['certdir'] = args.certdir
3b7e51
+        else:
3b7e51
+            print("certificate directory ({}) does not exist or is not a directory".format(args.certdir))
3b7e51
+            exit(1)
3b7e51
+
3b7e51
     # Initialize the options
3b7e51
     opts['binddn'] = args.binddn
3b7e51
     opts['bindpw'] = args.bindpw
3b7e51
     opts['suffix'] = args.suffix
3b7e51
-    opts['certdir'] = args.certdir
3b7e51
     opts['starttime'] = int(time.time())
3b7e51
     opts['verbose'] = args.verbose
3b7e51
     opts['mldif'] = args.mldif
3b7e51
@@ -1109,6 +1264,18 @@ def main():
3b7e51
 
3b7e51
     if opts['mldif'] is not None and opts['rldif'] is not None:
3b7e51
         print ("Performing offline report...")
3b7e51
+
3b7e51
+        # Validate LDIF files, must exist and not be empty
3b7e51
+        for ldif_dir in [opts['mldif'], opts['rldif']]:
3b7e51
+            if not os.path.exists(ldif_dir):
3b7e51
+                print ("LDIF file ({}) does not exist".format(ldif_dir))
3b7e51
+                exit(1)
3b7e51
+            if os.path.getsize(ldif_dir) == 0:
3b7e51
+                print ("LDIF file ({}) is empty".format(ldif_dir))
3b7e51
+                exit(1)
3b7e51
+        if opts['mldif'] == opts['rldif']:
3b7e51
+            print("The Master and Replica LDIF files must be different")
3b7e51
+            exit(1)
3b7e51
         do_offline_report(opts, OUTPUT_FILE)
3b7e51
     else:
3b7e51
         print ("Performing online report...")
3b7e51
@@ -1118,5 +1285,6 @@ def main():
3b7e51
         print('Finished writing report to "%s"' % (args.file))
3b7e51
         OUTPUT_FILE.close()
3b7e51
 
3b7e51
+
3b7e51
 if __name__ == '__main__':
3b7e51
     main()
3b7e51
-- 
3b7e51
2.17.0
3b7e51