9aadfb960434ad0ffe06b9616696f5241ba08ddb
[metze/samba/wip.git] / source4 / torture / drs / python / getncchanges.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Tests various schema replication scenarios
5 #
6 # Copyright (C) Catalyst.Net Ltd. 2017
7 #
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21
22 #
23 # Usage:
24 #  export DC1=dc1_dns_name
25 #  export DC2=dc2_dns_name
26 #  export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun
27 #  PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN getncchanges -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD"
28 #
29
30 import drs_base
31 import samba.tests
32 import ldb
33 from ldb import SCOPE_BASE
34 import random
35
36 from samba.dcerpc import drsuapi
37
38 class DrsReplicaSyncIntegrityTestCase(drs_base.DrsBaseTestCase):
39     def setUp(self):
40         super(DrsReplicaSyncIntegrityTestCase, self).setUp()
41
42         # Note that DC2 is the DC with the testenv-specific quirks (e.g. it's
43         # the vampire_dc), so we point this test directly at that DC
44         self.set_test_ldb_dc(self.ldb_dc2)
45         (self.drs, self.drs_handle) = self._ds_bind(self.dnsname_dc2)
46
47         # add some randomness to the test OU. (Deletion of the last test's
48         # objects can be slow to replicate out. So the OU created by a previous
49         # testenv may still exist at this point).
50         rand = random.randint(1, 10000000)
51         self.base_dn = self.test_ldb_dc.get_default_basedn()
52         self.ou = "OU=getncchanges%d_test,%s" %(rand, self.base_dn)
53         self.test_ldb_dc.add({
54             "dn": self.ou,
55             "objectclass": "organizationalUnit"})
56         (self.default_hwm, self.default_utdv) = self._get_highest_hwm_utdv(self.test_ldb_dc)
57
58         self.rxd_dn_list = []
59         self.rxd_links = []
60         self.rxd_guids = []
61
62         # 100 is the minimum max_objects that Microsoft seems to honour
63         # (the max honoured is 400ish), so we use that in these tests
64         self.max_objects = 100
65         self.last_ctr = None
66
67         # store whether we used GET_TGT/GET_ANC flags in the requests
68         self.used_get_tgt = False
69         self.used_get_anc = False
70
71     def tearDown(self):
72         super(DrsReplicaSyncIntegrityTestCase, self).tearDown()
73         # tidyup groups and users
74         try:
75             self.ldb_dc2.delete(self.ou, ["tree_delete:1"])
76         except ldb.LdbError as (enum, string):
77             if enum == ldb.ERR_NO_SUCH_OBJECT:
78                 pass
79
80     def add_object(self, dn):
81         """Adds an OU object"""
82         self.test_ldb_dc.add({"dn": dn, "objectclass": "organizationalunit"})
83         res = self.test_ldb_dc.search(base=dn, scope=SCOPE_BASE)
84         self.assertEquals(len(res), 1)
85
86     def modify_object(self, dn, attr, value):
87         """Modifies an object's USN by adding an attribute value to it"""
88         m = ldb.Message()
89         m.dn = ldb.Dn(self.test_ldb_dc, dn)
90         m[attr] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, attr)
91         self.test_ldb_dc.modify(m)
92
93     def delete_attribute(self, dn, attr, value):
94         """Deletes an attribute from an object"""
95         m = ldb.Message()
96         m.dn = ldb.Dn(self.ldb_dc2, dn)
97         m[attr] = ldb.MessageElement(value, ldb.FLAG_MOD_DELETE, attr)
98         self.ldb_dc2.modify(m)
99
100     def start_new_repl_cycle(self):
101         """Resets enough state info to start a new replication cycle"""
102         # reset rxd_links, but leave rxd_guids and rxd_dn_list alone so we know
103         # whether a parent/target is unknown and needs GET_ANC/GET_TGT to resolve
104         self.rxd_links = []
105
106         self.used_get_tgt = False
107         self.used_get_anc = False
108         # mostly preserve self.last_ctr, so that we use the last HWM
109         if self.last_ctr is not None:
110             self.last_ctr.more_data = True
111
112     def create_object_range(self, start, end, prefix="",
113                             children=None, parent_list=None):
114         """
115         Creates a block of objects. Object names are numbered sequentially,
116         using the optional prefix supplied. If the children parameter is
117         supplied it will create a parent-child hierarchy and return the
118         top-level parents separately.
119         """
120         dn_list = []
121
122         # Use dummy/empty lists if we're not creating a parent/child hierarchy
123         if children is None:
124             children = []
125
126         if parent_list is None:
127             parent_list = []
128
129         # Create the parents first, then the children.
130         # This makes it easier to see in debug when GET_ANC takes effect
131         # because the parent/children become interleaved (by default,
132         # this approach means the objects are organized into blocks of
133         # parents and blocks of children together)
134         for x in range(start, end):
135             ou = "OU=test_ou_%s%d,%s" % (prefix, x, self.ou)
136             self.add_object(ou)
137             dn_list.append(ou)
138
139             # keep track of the top-level parents (if needed)
140             parent_list.append(ou)
141
142         # create the block of children (if needed)
143         for x in range(start, end):
144             for child in children:
145                 ou = "OU=test_ou_child%s%d,%s" % (child, x, parent_list[x])
146                 self.add_object(ou)
147                 dn_list.append(ou)
148
149         return dn_list
150
151     def assert_expected_data(self, expected_list):
152         """
153         Asserts that we received all the DNs that we expected and
154         none are missing.
155         """
156         received_list = self.rxd_dn_list
157
158         # Note that with GET_ANC Windows can end up sending the same parent
159         # object multiple times, so this might be noteworthy but doesn't
160         # warrant failing the test
161         if (len(received_list) != len(expected_list)):
162             print("Note: received %d objects but expected %d" %(len(received_list),
163                                                                 len(expected_list)))
164
165         # Check that we received every object that we were expecting
166         for dn in expected_list:
167             self.assertTrue(dn in received_list, "DN '%s' missing from replication." % dn)
168
169     def test_repl_integrity(self):
170         """
171         Modify the objects being replicated while the replication is still
172         in progress and check that no object loss occurs.
173         """
174
175         # The server behaviour differs between samba and Windows. Samba returns
176         # the objects in the original order (up to the pre-modify HWM). Windows
177         # incorporates the modified objects and returns them in the new order
178         # (i.e. modified objects last), up to the post-modify HWM. The Microsoft
179         # docs state the Windows behaviour is optional.
180
181         # Create a range of objects to replicate.
182         expected_dn_list = self.create_object_range(0, 400)
183         (orig_hwm, unused) = self._get_highest_hwm_utdv(self.test_ldb_dc)
184
185         # We ask for the first page of 100 objects.
186         # For this test, we don't care what order we receive the objects in,
187         # so long as by the end we've received everything
188         self.repl_get_next()
189
190         # Modify some of the second page of objects. This should bump the highwatermark
191         for x in range(100, 200):
192             self.modify_object(expected_dn_list[x], "displayName", "OU%d" % x)
193
194         (post_modify_hwm, unused) = self._get_highest_hwm_utdv(self.test_ldb_dc)
195         self.assertTrue(post_modify_hwm.highest_usn > orig_hwm.highest_usn)
196
197         # Get the remaining blocks of data
198         while not self.replication_complete():
199             self.repl_get_next()
200
201         # Check we still receive all the objects we're expecting
202         self.assert_expected_data(expected_dn_list)
203
204     def is_parent_known(self, dn, known_dn_list):
205         """
206         Returns True if the parent of the dn specified is in known_dn_list
207         """
208
209         # we can sometimes get system objects like the RID Manager returned.
210         # Ignore anything that is not under the test OU we created
211         if self.ou not in dn:
212             return True
213
214         # Remove the child portion from the name to get the parent's DN
215         name_substrings = dn.split(",")
216         del name_substrings[0]
217
218         parent_dn = ",".join(name_substrings)
219
220         # check either this object is a parent (it's parent is the top-level
221         # test object), or its parent has been seen previously
222         return parent_dn == self.ou or parent_dn in known_dn_list
223
224     def _repl_send_request(self, get_anc=False, get_tgt=False):
225         """Sends a GetNCChanges request for the next block of replication data."""
226
227         # we're just trying to mimic regular client behaviour here, so just
228         # use the highwatermark in the last response we received
229         if self.last_ctr:
230             highwatermark = self.last_ctr.new_highwatermark
231             uptodateness_vector = self.last_ctr.uptodateness_vector
232         else:
233             # this is the first replication chunk
234             highwatermark = None
235             uptodateness_vector = None
236
237         # Ask for the next block of replication data
238         replica_flags = drsuapi.DRSUAPI_DRS_WRIT_REP
239         more_flags = 0
240
241         if get_anc:
242             replica_flags = drsuapi.DRSUAPI_DRS_WRIT_REP | drsuapi.DRSUAPI_DRS_GET_ANC
243             self.used_get_anc = True
244
245         if get_tgt:
246             more_flags = drsuapi.DRSUAPI_DRS_GET_TGT
247             self.used_get_tgt = True
248
249         # return the response from the DC
250         return self._get_replication(replica_flags,
251                                      max_objects=self.max_objects,
252                                      highwatermark=highwatermark,
253                                      uptodateness_vector=uptodateness_vector,
254                                      more_flags=more_flags)
255
256     def repl_get_next(self, get_anc=False, get_tgt=False, assert_links=False):
257         """
258         Requests the next block of replication data. This tries to simulate
259         client behaviour - if we receive a replicated object that we don't know
260         the parent of, then re-request the block with the GET_ANC flag set.
261         If we don't know the target object for a linked attribute, then
262         re-request with GET_TGT.
263         """
264
265         # send a request to the DC and get the response
266         ctr6 = self._repl_send_request(get_anc=get_anc, get_tgt=get_tgt)
267
268         # extract the object DNs and their GUIDs from the response
269         rxd_dn_list = self._get_ctr6_dn_list(ctr6)
270         rxd_guid_list = self._get_ctr6_object_guids(ctr6)
271
272         # we'll add new objects as we discover them, so take a copy of the
273         # ones we already know about, so we can modify these lists safely
274         known_objects = self.rxd_dn_list[:]
275         known_guids = self.rxd_guids[:]
276
277         # check that we know the parent for every object received
278         for i in range(0, len(rxd_dn_list)):
279
280             dn = rxd_dn_list[i]
281             guid = rxd_guid_list[i]
282
283             if self.is_parent_known(dn, known_objects):
284
285                 # the new DN is now known so add it to the list.
286                 # It may be the parent of another child in this block
287                 known_objects.append(dn)
288                 known_guids.append(guid)
289             else:
290                 # If we've already set the GET_ANC flag then it should mean
291                 # we receive the parents before the child
292                 self.assertFalse(get_anc, "Unknown parent for object %s" % dn)
293
294                 print("Unknown parent for %s - try GET_ANC" % dn)
295
296                 # try the same thing again with the GET_ANC flag set this time
297                 return self.repl_get_next(get_anc=True, get_tgt=get_tgt,
298                                           assert_links=assert_links)
299
300         # check we know about references to any objects in the linked attritbutes
301         received_links = self._get_ctr6_links(ctr6)
302
303         # This is so that older versions of Samba fail - we want the links to be
304         # sent roughly with the objects, rather than getting all links at the end
305         if assert_links:
306             self.assertTrue(len(received_links) > 0,
307                             "Links were expected in the GetNCChanges response")
308
309         for link in received_links:
310
311             # check the source object is known (Windows can actually send links
312             # where we don't know the source object yet). Samba shouldn't ever
313             # hit this case because it gets the links based on the source
314             if link.identifier not in known_guids:
315
316                 # If we've already set the GET_ANC flag then it should mean
317                 # this case doesn't happen
318                 self.assertFalse(get_anc, "Unknown source object for GUID %s"
319                                  % link.identifier)
320
321                 print("Unknown source GUID %s - try GET_ANC" % link.identifier)
322
323                 # try the same thing again with the GET_ANC flag set this time
324                 return self.repl_get_next(get_anc=True, get_tgt=get_tgt,
325                                           assert_links=assert_links)
326
327             # check we know the target object
328             if link.targetGUID not in known_guids:
329
330                 # If we've already set the GET_TGT flag then we should have
331                 # already received any objects we need to know about
332                 self.assertFalse(get_tgt, "Unknown linked target for object %s"
333                                  % link.targetDN)
334
335                 print("Unknown target for %s - try GET_TGT" % link.targetDN)
336
337                 # try the same thing again with the GET_TGT flag set this time
338                 return self.repl_get_next(get_anc=get_anc, get_tgt=True,
339                                           assert_links=assert_links)
340
341         # store the last successful result so we know what HWM to request next
342         self.last_ctr = ctr6
343
344         # store the objects, GUIDs, and links we received
345         self.rxd_dn_list += self._get_ctr6_dn_list(ctr6)
346         self.rxd_links += self._get_ctr6_links(ctr6)
347         self.rxd_guids += self._get_ctr6_object_guids(ctr6)
348
349         return ctr6
350
351     def replication_complete(self):
352         """Returns True if the current/last replication cycle is complete"""
353
354         if self.last_ctr is None or self.last_ctr.more_data:
355             return False
356         else:
357             return True
358
359     def test_repl_integrity_get_anc(self):
360         """
361         Modify the parent objects being replicated while the replication is still
362         in progress (using GET_ANC) and check that no object loss occurs.
363         """
364
365         # Note that GET_ANC behaviour varies between Windows and Samba.
366         # On Samba GET_ANC results in the replication restarting from the very
367         # beginning. After that, Samba remembers GET_ANC and also sends the
368         # parents in subsequent requests (regardless of whether GET_ANC is
369         # specified in the later request).
370         # Windows only sends the parents if GET_ANC was specified in the last
371         # request. It will also resend a parent, even if it's already sent the
372         # parent in a previous response (whereas Samba doesn't).
373
374         # Create a small block of 50 parents, each with 2 children (A and B)
375         # This is so that we receive some children in the first block, so we
376         # can resend with GET_ANC before we learn too many parents
377         parent_dn_list = []
378         expected_dn_list = self.create_object_range(0, 50, prefix="parent",
379                                                     children=("A", "B"),
380                                                     parent_list=parent_dn_list)
381
382         # create the remaining parents and children
383         expected_dn_list += self.create_object_range(50, 150, prefix="parent",
384                                                      children=("A", "B"),
385                                                      parent_list=parent_dn_list)
386
387         # We've now got objects in the following order:
388         # [50 parents][100 children][100 parents][200 children]
389
390         # Modify the first parent so that it's now ordered last by USN
391         # This means we set the GET_ANC flag pretty much straight away
392         # because we receive the first child before the first parent
393         self.modify_object(parent_dn_list[0], "displayName", "OU0")
394
395         # modify a later block of parents so they also get reordered
396         for x in range(50, 100):
397             self.modify_object(parent_dn_list[x], "displayName", "OU%d" % x)
398
399         # Get the first block of objects - this should resend the request with
400         # GET_ANC set because we won't know about the first child's parent.
401         # On samba GET_ANC essentially starts the sync from scratch again, so
402         # we get this over with early before we learn too many parents
403         self.repl_get_next()
404
405         # modify the last chunk of parents. They should now have a USN higher
406         # than the highwater-mark for the replication cycle
407         for x in range(100, 150):
408             self.modify_object(parent_dn_list[x], "displayName", "OU%d" % x)
409
410         # Get the remaining blocks of data - this will resend the request with
411         # GET_ANC if it encounters an object it doesn't have the parent for.
412         while not self.replication_complete():
413             self.repl_get_next()
414
415         # The way the test objects have been created should force
416         # self.repl_get_next() to use the GET_ANC flag. If this doesn't
417         # actually happen, then the test isn't doing its job properly
418         self.assertTrue(self.used_get_anc,
419                         "Test didn't use the GET_ANC flag as expected")
420
421         # Check we get all the objects we're expecting
422         self.assert_expected_data(expected_dn_list)
423
424     def assert_expected_links(self, objects_with_links, link_attr="managedBy",
425                               num_expected=None):
426         """
427         Asserts that a GetNCChanges response contains any expected links
428         for the objects it contains.
429         """
430         received_links = self.rxd_links
431
432         if num_expected is None:
433             num_expected = len(objects_with_links)
434
435         self.assertTrue(len(received_links) == num_expected,
436                         "Received %d links but expected %d"
437                         %(len(received_links), num_expected))
438
439         for dn in objects_with_links:
440             self.assert_object_has_link(dn, link_attr, received_links)
441
442     def assert_object_has_link(self, dn, link_attr, received_links):
443         """
444         Queries the object in the DB and asserts there is a link in the
445         GetNCChanges response that matches.
446         """
447
448         # Look up the link attribute in the DB
449         # The extended_dn option will dump the GUID info for the link
450         # attribute (as a hex blob)
451         res = self.test_ldb_dc.search(ldb.Dn(self.test_ldb_dc, dn), attrs=[link_attr],
452                                       controls=['extended_dn:1:0'], scope=ldb.SCOPE_BASE)
453
454         # We didn't find the expected link attribute in the DB for the object.
455         # Something has gone wrong somewhere...
456         self.assertTrue(link_attr in res[0], "%s in DB doesn't have attribute %s"
457                         %(dn, link_attr))
458
459         # find the received link in the list and assert that the target and
460         # source GUIDs match what's in the DB
461         for val in res[0][link_attr]:
462             # Work out the expected source and target GUIDs for the DB link
463             target_dn = ldb.Dn(self.test_ldb_dc, val)
464             targetGUID_blob = target_dn.get_extended_component("GUID")
465             sourceGUID_blob = res[0].dn.get_extended_component("GUID")
466
467             found = False
468
469             for link in received_links:
470                 if link.selfGUID_blob == sourceGUID_blob and \
471                    link.targetGUID_blob == targetGUID_blob:
472
473                     found = True
474
475                     if self._debug:
476                         print("Link %s --> %s" %(dn[:25], link.targetDN[:25]))
477                     break
478
479             self.assertTrue(found, "Did not receive expected link for DN %s" % dn)
480
481     def test_repl_get_tgt(self):
482         """
483         Creates a scenario where we should receive the linked attribute before
484         we know about the target object, and therefore need to use GET_TGT.
485         Note: Samba currently avoids this problem by sending all its links last
486         """
487
488         # create the test objects
489         reportees = self.create_object_range(0, 100, prefix="reportee")
490         managers = self.create_object_range(0, 100, prefix="manager")
491         all_objects = managers + reportees
492         expected_links = reportees
493
494         # add a link attribute to each reportee object that points to the
495         # corresponding manager object as the target
496         for i in range(0, 100):
497             self.modify_object(reportees[i], "managedBy", managers[i])
498
499         # touch the managers (the link-target objects) again to make sure the
500         # reportees (link source objects) get returned first by the replication
501         for i in range(0, 100):
502             self.modify_object(managers[i], "displayName", "OU%d" % i)
503
504         links_expected = True
505
506         # Get all the replication data - this code should resend the requests
507         # with GET_TGT
508         while not self.replication_complete():
509
510             # get the next block of replication data (this sets GET_TGT if needed)
511             self.repl_get_next(assert_links=links_expected)
512             links_expected = len(self.rxd_links) < len(expected_links)
513
514         # The way the test objects have been created should force
515         # self.repl_get_next() to use the GET_TGT flag. If this doesn't
516         # actually happen, then the test isn't doing its job properly
517         self.assertTrue(self.used_get_tgt,
518                         "Test didn't use the GET_TGT flag as expected")
519
520         # Check we get all the objects we're expecting
521         self.assert_expected_data(all_objects)
522
523         # Check we received links for all the reportees
524         self.assert_expected_links(expected_links)
525
526     def test_repl_get_tgt_chain(self):
527         """
528         Tests the behaviour of GET_TGT with a more complicated scenario.
529         Here we create a chain of objects linked together, so if we follow
530         the link target, then we'd traverse ~200 objects each time.
531         """
532
533         # create the test objects
534         objectsA = self.create_object_range(0, 100, prefix="AAA")
535         objectsB = self.create_object_range(0, 100, prefix="BBB")
536         objectsC = self.create_object_range(0, 100, prefix="CCC")
537
538         # create a complex set of object links:
539         #   A0-->B0-->C1-->B2-->C3-->B4-->and so on...
540         # Basically each object-A should link to a circular chain of 200 B/C
541         # objects. We create the links in separate chunks here, as it makes it
542         # clearer what happens with the USN (links on Windows have their own
543         # USN, so this approach means the A->B/B->C links aren't interleaved)
544         for i in range(0, 100):
545             self.modify_object(objectsA[i], "managedBy", objectsB[i])
546
547         for i in range(0, 100):
548             self.modify_object(objectsB[i], "managedBy", objectsC[(i + 1) % 100])
549
550         for i in range(0, 100):
551             self.modify_object(objectsC[i], "managedBy", objectsB[(i + 1) % 100])
552
553         all_objects = objectsA + objectsB + objectsC
554         expected_links = all_objects
555
556         # the default order the objects now get returned in should be:
557         # [A0-A99][B0-B99][C0-C99]
558
559         links_expected = True
560
561         # Get all the replication data - this code should resend the requests
562         # with GET_TGT
563         while not self.replication_complete():
564
565             # get the next block of replication data (this sets GET_TGT if needed)
566             self.repl_get_next(assert_links=links_expected)
567             links_expected = len(self.rxd_links) < len(expected_links)
568
569         # The way the test objects have been created should force
570         # self.repl_get_next() to use the GET_TGT flag. If this doesn't
571         # actually happen, then the test isn't doing its job properly
572         self.assertTrue(self.used_get_tgt,
573                         "Test didn't use the GET_TGT flag as expected")
574
575         # Check we get all the objects we're expecting
576         self.assert_expected_data(all_objects)
577
578         # Check we received links for all the reportees
579         self.assert_expected_links(expected_links)
580
581     def test_repl_integrity_link_attr(self):
582         """
583         Tests adding links to new objects while a replication is in progress.
584         """
585
586         # create some source objects for the linked attributes, sandwiched
587         # between 2 blocks of filler objects
588         filler = self.create_object_range(0, 100, prefix="filler")
589         reportees = self.create_object_range(0, 100, prefix="reportee")
590         filler += self.create_object_range(100, 200, prefix="filler")
591
592         # Start the replication and get the first block of filler objects
593         # (We're being mean here and setting the GET_TGT flag right from the
594         # start. On earlier Samba versions, if the client encountered an
595         # unknown target object and retried with GET_TGT, it would restart the
596         # replication cycle from scratch, which avoids the problem).
597         self.repl_get_next(get_tgt=True)
598
599         # create the target objects and add the links. These objects should be
600         # outside the scope of the Samba replication cycle, but the links should
601         # still get sent with the source object
602         managers = self.create_object_range(0, 100, prefix="manager")
603
604         for i in range(0, 100):
605             self.modify_object(reportees[i], "managedBy", managers[i])
606
607         expected_objects = managers + reportees + filler
608         expected_links = reportees
609
610         # complete the replication
611         while not self.replication_complete():
612             self.repl_get_next(get_tgt=True)
613
614         # If we didn't receive the most recently created objects in the last
615         # replication cycle, then kick off another replication to get them
616         if len(self.rxd_dn_list) < len(expected_objects):
617             self.repl_get_next()
618
619             while not self.replication_complete():
620                 self.repl_get_next()
621
622         # Check we get all the objects we're expecting
623         self.assert_expected_data(expected_objects)
624
625         # Check we received links for all the parents
626         self.assert_expected_links(expected_links)
627
628     def test_repl_get_anc_link_attr(self):
629         """
630         A basic GET_ANC test where the parents have linked attributes
631         """
632
633         # Create a block of 100 parents and 100 children
634         parent_dn_list = []
635         expected_dn_list = self.create_object_range(0, 100, prefix="parent",
636                                                     children=("A"),
637                                                     parent_list=parent_dn_list)
638
639         # Add links from the parents to the children
640         for x in range(0, 100):
641             self.modify_object(parent_dn_list[x], "managedBy", expected_dn_list[x + 100])
642
643         # add some filler objects at the end. This allows us to easily see
644         # which chunk the links get sent in
645         expected_dn_list += self.create_object_range(0, 100, prefix="filler")
646
647         # We've now got objects in the following order:
648         # [100 x children][100 x parents][100 x filler]
649
650         # Get the replication data - because the block of children come first,
651         # this should retry the request with GET_ANC
652         while not self.replication_complete():
653             self.repl_get_next()
654
655         self.assertTrue(self.used_get_anc,
656                         "Test didn't use the GET_ANC flag as expected")
657
658         # Check we get all the objects we're expecting
659         self.assert_expected_data(expected_dn_list)
660
661         # Check we received links for all the parents
662         self.assert_expected_links(parent_dn_list)
663
664     def test_repl_get_tgt_and_anc(self):
665         """
666         Check we can resolve an unknown ancestor when fetching the link target,
667         i.e. tests using GET_TGT and GET_ANC in combination
668         """
669
670         # Create some parent/child objects (the child will be the link target)
671         parents = []
672         all_objects = self.create_object_range(0, 100, prefix="parent",
673                                                children=["la_tgt"],
674                                                parent_list=parents)
675
676         children = [item for item in all_objects if item not in parents]
677
678         # create the link source objects and link them to the child/target
679         la_sources = self.create_object_range(0, 100, prefix="la_src")
680         all_objects += la_sources
681
682         for i in range(0, 100):
683             self.modify_object(la_sources[i], "managedBy", children[i])
684
685         expected_links = la_sources
686
687         # modify the children/targets so they come after the link source
688         for x in range(0, 100):
689             self.modify_object(children[x], "displayName", "OU%d" % x)
690
691         # modify the parents, so they now come last in the replication
692         for x in range(0, 100):
693             self.modify_object(parents[x], "displayName", "OU%d" % x)
694
695         # We've now got objects in the following order:
696         # [100 la_source][100 la_target][100 parents (of la_target)]
697
698         links_expected = True
699
700         # Get all the replication data - this code should resend the requests
701         # with GET_TGT and GET_ANC
702         while not self.replication_complete():
703
704             # get the next block of replication data (this sets GET_TGT/GET_ANC)
705             self.repl_get_next(assert_links=links_expected)
706             links_expected = len(self.rxd_links) < len(expected_links)
707
708         # The way the test objects have been created should force
709         # self.repl_get_next() to use the GET_TGT/GET_ANC flags. If this
710         # doesn't actually happen, then the test isn't doing its job properly
711         self.assertTrue(self.used_get_tgt,
712                         "Test didn't use the GET_TGT flag as expected")
713         self.assertTrue(self.used_get_anc,
714                         "Test didn't use the GET_ANC flag as expected")
715
716         # Check we get all the objects we're expecting
717         self.assert_expected_data(all_objects)
718
719         # Check we received links for all the link sources
720         self.assert_expected_links(expected_links)
721
722         # Second part of test. Add some extra objects and kick off another
723         # replication. The test code will use the HWM from the last replication
724         # so we'll only receive the objects we modify below
725         self.start_new_repl_cycle()
726
727         # add an extra level of grandchildren that hang off a child
728         # that got created last time
729         new_parent = "OU=test_new_parent,%s" % children[0]
730         self.add_object(new_parent)
731         new_children = []
732
733         for x in range(0, 50):
734             dn = "OU=test_new_la_tgt%d,%s" % (x, new_parent)
735             self.add_object(dn)
736             new_children.append(dn)
737
738         # replace half of the links to point to the new children
739         for x in range(0, 50):
740             self.delete_attribute(la_sources[x], "managedBy", children[x])
741             self.modify_object(la_sources[x], "managedBy", new_children[x])
742
743         # add some filler objects to fill up the 1st chunk
744         filler = self.create_object_range(0, 100, prefix="filler")
745
746         # modify the new children/targets so they come after the link source
747         for x in range(0, 50):
748             self.modify_object(new_children[x], "displayName", "OU-%d" % x)
749
750         # modify the parent, so it now comes last in the replication
751         self.modify_object(new_parent, "displayName", "OU%d" % x)
752
753         # We should now get the modified objects in the following order:
754         # [50 links (x 2)][100 filler][50 new children][new parent]
755         # Note that the link sources aren't actually sent (their new linked
756         # attributes are sent, but apart from that, nothing has changed)
757         all_objects = filler + new_children + [new_parent]
758         expected_links = la_sources[:50]
759
760         links_expected = True
761
762         while not self.replication_complete():
763             self.repl_get_next(assert_links=links_expected)
764             links_expected = len(self.rxd_links) < len(expected_links)
765
766         self.assertTrue(self.used_get_tgt,
767                         "Test didn't use the GET_TGT flag as expected")
768         self.assertTrue(self.used_get_anc,
769                         "Test didn't use the GET_ANC flag as expected")
770
771         # Check we get all the objects we're expecting
772         self.assert_expected_data(all_objects)
773
774         # Check we received links (50 deleted links and 50 new)
775         self.assert_expected_links(expected_links, num_expected=100)
776