|
|
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 |
|