1
0
mirror of https://github.com/samba-team/samba.git synced 2025-03-10 12:58:35 +03:00

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
This commit is contained in:
Tim Beale 2017-06-06 18:21:40 +12:00 committed by Andrew Bartlett
parent 4cfc296885
commit 9f0ae6e44d
2 changed files with 212 additions and 8 deletions

View File

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

View File

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