summaryrefslogtreecommitdiff
path: root/source4/torture/drs
diff options
context:
space:
mode:
authorTim Beale <timbeale@catalyst.net.nz>2017-06-06 18:21:40 +1200
committerAndrew Bartlett <abartlet@samba.org>2017-08-18 06:07:12 +0200
commit9f0ae6e44d0f6def6be7c44067c7fcfdf0d42db2 (patch)
treef2dd3b6c1d7c4fe7e92c0fe5d3af25cdd9e7c98c /source4/torture/drs
parent4cfc29688584ca69c43abb770d1e721d1eab1480 (diff)
downloadsamba-9f0ae6e44d0f6def6be7c44067c7fcfdf0d42db2.tar.gz
getncchanges.py: Add GET_ANC replication test case
This test: - creates blocks of parent/child objects - modifies the parents, so the child gets received first in the replication (which means the client has to use GET_ANC) - checks that we always receive the parent before the child (if not, it either retries with GET_ANC, or asserts if GET_ANC is already set) - modifies the parent objects to change their USN while the replication is in progress - checks that all expected objects are received by the end of the test I've added a repl_get_next() function to help simulate a client's behaviour - if it encounters an object it doesn't know the parent of, then it retries with GET_ANC. Also added some debug to drs_base.py that developers can turn on to make it easier to see what objects we're actually receiving in the responses. Signed-off-by: Tim Beale <timbeale@catalyst.net.nz> Reviewed-by: Garming Sam <garming@samba.org> Reviewed-by: Andrew Bartlett <abartlet@samba.org> BUG: https://bugzilla.samba.org/show_bug.cgi?id=12972
Diffstat (limited to 'source4/torture/drs')
-rw-r--r--source4/torture/drs/python/drs_base.py25
-rw-r--r--source4/torture/drs/python/getncchanges.py195
2 files changed, 212 insertions, 8 deletions
diff --git a/source4/torture/drs/python/drs_base.py b/source4/torture/drs/python/drs_base.py
index b37832601e5..b2df1812b8e 100644
--- a/source4/torture/drs/python/drs_base.py
+++ b/source4/torture/drs/python/drs_base.py
@@ -68,6 +68,9 @@ class DrsBaseTestCase(SambaToolCmdTest):
self.dnsname_dc1 = self.info_dc1["dnsHostName"][0]
self.dnsname_dc2 = self.info_dc2["dnsHostName"][0]
+ # for debugging the test code
+ self._debug = False
+
def tearDown(self):
super(DrsBaseTestCase, self).tearDown()
@@ -194,6 +197,27 @@ class DrsBaseTestCase(SambaToolCmdTest):
id.dn = str(res[0].dn)
return id
+ def _ctr6_debug(self, ctr6):
+ """
+ Displays basic info contained in a DsGetNCChanges response.
+ Having this debug code allows us to see the difference in behaviour
+ between Samba and Windows easier. Turn on the self._debug flag to see it.
+ """
+
+ if self._debug:
+ print("------------ recvd CTR6 -------------")
+
+ next_object = ctr6.first_object
+ for i in range(0, ctr6.object_count):
+ print("Obj %d: %s %s" %(i, next_object.object.identifier.dn[:22],
+ next_object.object.identifier.guid))
+ next_object = next_object.next_object
+
+ print("Linked Attributes: %d" % ctr6.linked_attributes_count)
+ print("HWM: %d" %(ctr6.new_highwatermark.highest_usn))
+ print("Tmp HWM: %d" %(ctr6.new_highwatermark.tmp_highest_usn))
+ print("More data: %d" %(ctr6.more_data))
+
def _get_replication(self, replica_flags,
drs_error=drsuapi.DRSUAPI_EXOP_ERR_NONE, drs=None, drs_handle=None,
highwatermark=None, uptodateness_vector=None,
@@ -242,6 +266,7 @@ class DrsBaseTestCase(SambaToolCmdTest):
uptodateness_vector_v1.cursors = cursors
req10.uptodateness_vector = uptodateness_vector_v1
(level, ctr) = drs.DsGetNCChanges(drs_handle, 10, req10)
+ self._ctr6_debug(ctr)
self.assertEqual(level, 6, "expected level 6 response!")
self.assertEqual(ctr.source_dsa_guid, misc.GUID(source_dsa))
diff --git a/source4/torture/drs/python/getncchanges.py b/source4/torture/drs/python/getncchanges.py
index d1d6b2bb67d..2f914d8dd40 100644
--- a/source4/torture/drs/python/getncchanges.py
+++ b/source4/torture/drs/python/getncchanges.py
@@ -44,7 +44,14 @@ class DrsReplicaSyncIntegrityTestCase(drs_base.DrsBaseTestCase):
"objectclass": "organizationalUnit"})
(self.drs, self.drs_handle) = self._ds_bind(self.dnsname_dc1)
(self.default_hwm, self.default_utdv) = self._get_highest_hwm_utdv(self.ldb_dc1)
- self._debug = True
+
+ # 100 is the minimum max_objects that Microsoft seems to honour
+ # (the max honoured is 400ish), so we use that in these tests
+ self.max_objects = 100
+ self.last_ctr = None
+
+ # store whether we used GET_ANC flags in the requests
+ self.used_get_anc = False
def tearDown(self):
super(DrsReplicaSyncIntegrityTestCase, self).tearDown()
@@ -68,13 +75,23 @@ class DrsReplicaSyncIntegrityTestCase(drs_base.DrsBaseTestCase):
m[attr] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, attr)
self.ldb_dc1.modify(m)
- def create_object_range(self, start, end, prefix=""):
+ def create_object_range(self, start, end, prefix="",
+ children=None, parent_list=None):
"""
Creates a block of objects. Object names are numbered sequentially,
- using the optional prefix supplied.
+ using the optional prefix supplied. If the children parameter is
+ supplied it will create a parent-child hierarchy and return the
+ top-level parents separately.
"""
dn_list = []
+ # Use dummy/empty lists if we're not creating a parent/child hierarchy
+ if children is None:
+ children = []
+
+ if parent_list is None:
+ parent_list = []
+
# Create the parents first, then the children.
# This makes it easier to see in debug when GET_ANC takes effect
# because the parent/children become interleaved (by default,
@@ -85,6 +102,16 @@ class DrsReplicaSyncIntegrityTestCase(drs_base.DrsBaseTestCase):
self.add_object(ou)
dn_list.append(ou)
+ # keep track of the top-level parents (if needed)
+ parent_list.append(ou)
+
+ # create the block of children (if needed)
+ for x in range(start, end):
+ for child in children:
+ ou = "OU=test_ou_child%s%d,%s" % (child, x, parent_list[x])
+ self.add_object(ou)
+ dn_list.append(ou)
+
return dn_list
def assert_expected_data(self, received_list, expected_list):
@@ -124,7 +151,7 @@ class DrsReplicaSyncIntegrityTestCase(drs_base.DrsBaseTestCase):
# For this test, we don't care what order we receive the objects in,
# so long as by the end we've received everything
rxd_dn_list = []
- ctr6 = self._get_replication(drsuapi.DRSUAPI_DRS_WRIT_REP, max_objects=100)
+ ctr6 = self.repl_get_next(rxd_dn_list)
rxd_dn_list = self._get_ctr6_dn_list(ctr6)
# Modify some of the second page of objects. This should bump the highwatermark
@@ -135,13 +162,165 @@ class DrsReplicaSyncIntegrityTestCase(drs_base.DrsBaseTestCase):
self.assertTrue(post_modify_hwm.highest_usn > orig_hwm.highest_usn)
# Get the remaining blocks of data
- while ctr6.more_data:
- ctr6 = self._get_replication(drsuapi.DRSUAPI_DRS_WRIT_REP, max_objects=100,
- highwatermark=ctr6.new_highwatermark,
- uptodateness_vector=ctr6.uptodateness_vector)
+ while not self.replication_complete():
+ ctr6 = self.repl_get_next(rxd_dn_list)
rxd_dn_list += self._get_ctr6_dn_list(ctr6)
# Check we still receive all the objects we're expecting
self.assert_expected_data(rxd_dn_list, expected_dn_list)
+ def is_parent_known(self, dn, known_dn_list):
+ """
+ Returns True if the parent of the dn specified is in known_dn_list
+ """
+
+ # we can sometimes get system objects like the RID Manager returned.
+ # Ignore anything that is not under the test OU we created
+ if self.ou not in dn:
+ return True
+
+ # Remove the child portion from the name to get the parent's DN
+ name_substrings = dn.split(",")
+ del name_substrings[0]
+
+ parent_dn = ",".join(name_substrings)
+
+ # check either this object is a parent (it's parent is the top-level
+ # test object), or its parent has been seen previously
+ return parent_dn == self.ou or parent_dn in known_dn_list
+
+ def repl_get_next(self, initial_objects, get_anc=False):
+ """
+ Requests the next block of replication data. This tries to simulate
+ client behaviour - if we receive a replicated object that we don't know
+ the parent of, then re-request the block with the GET_ANC flag set.
+ """
+
+ # we're just trying to mimic regular client behaviour here, so just
+ # use the highwatermark in the last response we received
+ if self.last_ctr:
+ highwatermark = self.last_ctr.new_highwatermark
+ uptodateness_vector = self.last_ctr.uptodateness_vector
+ else:
+ # this is the initial replication, so we're starting from the start
+ highwatermark = None
+ uptodateness_vector = None
+
+ # we'll add new objects as we discover them, so take a copy to modify
+ known_objects = initial_objects[:]
+
+ # Ask for the next block of replication data
+ replica_flags = drsuapi.DRSUAPI_DRS_WRIT_REP
+
+ if get_anc:
+ replica_flags = drsuapi.DRSUAPI_DRS_WRIT_REP | drsuapi.DRSUAPI_DRS_GET_ANC
+ self.used_get_anc = True
+
+ ctr6 = self._get_replication(replica_flags,
+ max_objects=self.max_objects,
+ highwatermark=highwatermark,
+ uptodateness_vector=uptodateness_vector)
+
+ # check that we know the parent for every object received
+ rxd_dn_list = self._get_ctr6_dn_list(ctr6)
+
+ for i in range(0, len(rxd_dn_list)):
+
+ dn = rxd_dn_list[i]
+
+ if self.is_parent_known(dn, known_objects):
+
+ # the new DN is now known so add it to the list.
+ # It may be the parent of another child in this block
+ known_objects.append(dn)
+ else:
+ # If we've already set the GET_ANC flag then it should mean
+ # we receive the parents before the child
+ self.assertFalse(get_anc, "Unknown parent for object %s" % dn)
+
+ print("Unknown parent for %s - try GET_ANC" % dn)
+
+ # try the same thing again with the GET_ANC flag set this time
+ return self.repl_get_next(get_anc=True)
+
+ # store the last successful result so we know what HWM to request next
+ self.last_ctr = ctr6
+
+ return ctr6
+
+ def replication_complete(self):
+ """Returns True if the current/last replication cycle is complete"""
+
+ if self.last_ctr is None or self.last_ctr.more_data:
+ return False
+ else:
+ return True
+
+ def test_repl_integrity_get_anc(self):
+ """
+ Modify the parent objects being replicated while the replication is still
+ in progress (using GET_ANC) and check that no object loss occurs.
+ """
+
+ # Note that GET_ANC behaviour varies between Windows and Samba.
+ # On Samba GET_ANC results in the replication restarting from the very
+ # beginning. After that, Samba remembers GET_ANC and also sends the
+ # parents in subsequent requests (regardless of whether GET_ANC is
+ # specified in the later request).
+ # Windows only sends the parents if GET_ANC was specified in the last
+ # request. It will also resend a parent, even if it's already sent the
+ # parent in a previous response (whereas Samba doesn't).
+
+ # Create a small block of 50 parents, each with 2 children (A and B)
+ # This is so that we receive some children in the first block, so we
+ # can resend with GET_ANC before we learn too many parents
+ parent_dn_list = []
+ expected_dn_list = self.create_object_range(0, 50, prefix="parent",
+ children=("A", "B"),
+ parent_list=parent_dn_list)
+
+ # create the remaining parents and children
+ expected_dn_list += self.create_object_range(50, 150, prefix="parent",
+ children=("A", "B"),
+ parent_list=parent_dn_list)
+
+ # We've now got objects in the following order:
+ # [50 parents][100 children][100 parents][200 children]
+
+ # Modify the first parent so that it's now ordered last by USN
+ # This means we set the GET_ANC flag pretty much straight away
+ # because we receive the first child before the first parent
+ self.modify_object(parent_dn_list[0], "displayName", "OU0")
+
+ # modify a later block of parents so they also get reordered
+ for x in range(50, 100):
+ self.modify_object(parent_dn_list[x], "displayName", "OU%d" % x)
+
+ # Get the first block of objects - this should resend the request with
+ # GET_ANC set because we won't know about the first child's parent.
+ # On samba GET_ANC essentially starts the sync from scratch again, so
+ # we get this over with early before we learn too many parents
+ rxd_dn_list = []
+ ctr6 = self.repl_get_next(rxd_dn_list)
+ rxd_dn_list = self._get_ctr6_dn_list(ctr6)
+
+ # modify the last chunk of parents. They should now have a USN higher
+ # than the highwater-mark for the replication cycle
+ for x in range(100, 150):
+ self.modify_object(parent_dn_list[x], "displayName", "OU%d" % x)
+
+ # Get the remaining blocks of data - this will resend the request with
+ # GET_ANC if it encounters an object it doesn't have the parent for.
+ while not self.replication_complete():
+ ctr6 = self.repl_get_next(rxd_dn_list)
+ rxd_dn_list += self._get_ctr6_dn_list(ctr6)
+
+ # The way the test objects have been created should force
+ # self.repl_get_next() to use the GET_ANC flag. If this doesn't
+ # actually happen, then the test isn't doing its job properly
+ self.assertTrue(self.used_get_anc,
+ "Test didn't use the GET_ANC flag as expected")
+
+ # Check we get all the objects we're expecting
+ self.assert_expected_data(rxd_dn_list, expected_dn_list)