2 # -*- coding: utf-8 -*-
4 # Tests various schema replication scenarios
6 # Copyright (C) Catalyst.Net Ltd. 2017
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.
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.
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/>.
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 \
28 # getncchanges -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD"
34 from ldb
import SCOPE_BASE
37 from samba
.dcerpc
import drsuapi
, misc
38 from samba
import WERRORError
39 from samba
import werror
41 class DrsReplicaSyncIntegrityTestCase(drs_base
.DrsBaseTestCase
):
43 super(DrsReplicaSyncIntegrityTestCase
, self
).setUp()
45 self
.init_test_state()
47 # Note that DC2 is the DC with the testenv-specific quirks (e.g. it's
48 # the vampire_dc), so we point this test directly at that DC
49 self
.set_test_ldb_dc(self
.ldb_dc2
)
51 self
.ou
= str(samba
.tests
.create_test_ou(self
.test_ldb_dc
,
52 "getncchanges." + self
.id().rsplit(".", 1)[1]))
54 self
.addCleanup(self
.ldb_dc2
.delete
, self
.ou
, ["tree_delete:1"])
56 self
.base_dn
= self
.test_ldb_dc
.get_default_basedn()
58 self
.default_conn
= DcConnection(self
, self
.ldb_dc2
, self
.dnsname_dc2
)
59 self
.set_dc_connection(self
.default_conn
)
61 def init_test_state(self
):
67 # 100 is the minimum max_objects that Microsoft seems to honour
68 # (the max honoured is 400ish), so we use that in these tests
69 self
.max_objects
= 100
71 # store whether we used GET_TGT/GET_ANC flags in the requests
72 self
.used_get_tgt
= False
73 self
.used_get_anc
= False
75 def add_object(self
, dn
, objectclass
="organizationalunit"):
76 """Adds an OU object"""
77 self
.test_ldb_dc
.add({"dn": dn
, "objectclass": objectclass
})
78 res
= self
.test_ldb_dc
.search(base
=dn
, scope
=SCOPE_BASE
)
79 self
.assertEqual(len(res
), 1)
81 def modify_object(self
, dn
, attr
, value
):
82 """Modifies an object's USN by adding an attribute value to it"""
84 m
.dn
= ldb
.Dn(self
.test_ldb_dc
, dn
)
85 m
[attr
] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_ADD
, attr
)
86 self
.test_ldb_dc
.modify(m
)
88 def delete_attribute(self
, dn
, attr
, value
):
89 """Deletes an attribute from an object"""
91 m
.dn
= ldb
.Dn(self
.test_ldb_dc
, dn
)
92 m
[attr
] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_DELETE
, attr
)
93 self
.test_ldb_dc
.modify(m
)
95 def start_new_repl_cycle(self
):
96 """Resets enough state info to start a new replication cycle"""
97 # reset rxd_links, but leave rxd_guids and rxd_dn_list alone so we know
98 # whether a parent/target is unknown and needs GET_ANC/GET_TGT to
102 self
.used_get_tgt
= False
103 self
.used_get_anc
= False
104 # mostly preserve self.last_ctr, so that we use the last HWM
105 if self
.last_ctr
is not None:
106 self
.last_ctr
.more_data
= True
108 def create_object_range(self
, start
, end
, prefix
="",
109 children
=None, parent_list
=None):
111 Creates a block of objects. Object names are numbered sequentially,
112 using the optional prefix supplied. If the children parameter is
113 supplied it will create a parent-child hierarchy and return the
114 top-level parents separately.
118 # Use dummy/empty lists if we're not creating a parent/child hierarchy
122 if parent_list
is None:
125 # Create the parents first, then the children.
126 # This makes it easier to see in debug when GET_ANC takes effect
127 # because the parent/children become interleaved (by default,
128 # this approach means the objects are organized into blocks of
129 # parents and blocks of children together)
130 for x
in range(start
, end
):
131 ou
= "OU=test_ou_%s%d,%s" % (prefix
, x
, self
.ou
)
135 # keep track of the top-level parents (if needed)
136 parent_list
.append(ou
)
138 # create the block of children (if needed)
139 for x
in range(start
, end
):
140 for child
in children
:
141 ou
= "OU=test_ou_child%s%d,%s" % (child
, x
, parent_list
[x
])
147 def assert_expected_data(self
, expected_list
):
149 Asserts that we received all the DNs that we expected and
152 received_list
= self
.rxd_dn_list
154 # Note that with GET_ANC Windows can end up sending the same parent
155 # object multiple times, so this might be noteworthy but doesn't
156 # warrant failing the test
157 num_received
= len(received_list
)
158 num_expected
= len(expected_list
)
159 if num_received
!= num_expected
:
160 print("Note: received %d objects but expected %d" % (num_received
,
163 # Check that we received every object that we were expecting
164 for dn
in expected_list
:
165 self
.assertTrue(dn
in received_list
,
166 "DN '%s' missing from replication." % dn
)
168 def test_repl_integrity(self
):
170 Modify the objects being replicated while the replication is still
171 in progress and check that no object loss occurs.
174 # The server behaviour differs between samba and Windows. Samba returns
175 # the objects in the original order (up to the pre-modify HWM). Windows
176 # incorporates the modified objects and returns them in the new order
177 # (i.e. modified objects last), up to the post-modify HWM. The
178 # Microsoft docs state the Windows behaviour is optional.
180 # Create a range of objects to replicate.
181 expected_dn_list
= self
.create_object_range(0, 400)
182 (orig_hwm
, unused
) = self
._get
_highest
_hwm
_utdv
(self
.test_ldb_dc
)
184 # We ask for the first page of 100 objects.
185 # For this test, we don't care what order we receive the objects in,
186 # so long as by the end we've received everything
189 # Modify some of the second page of objects. This should bump the
191 for x
in range(100, 200):
192 self
.modify_object(expected_dn_list
[x
], "displayName", "OU%d" % x
)
194 (post_modify_hwm
, _
) = self
._get
_highest
_hwm
_utdv
(self
.test_ldb_dc
)
195 self
.assertTrue(post_modify_hwm
.highest_usn
> orig_hwm
.highest_usn
)
197 # Get the remaining blocks of data
198 while not self
.replication_complete():
201 # Check we still receive all the objects we're expecting
202 self
.assert_expected_data(expected_dn_list
)
204 def is_parent_known(self
, dn
, known_dn_list
):
206 Returns True if the parent of the dn specified is in known_dn_list
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
:
214 # Remove the child portion from the name to get the parent's DN
215 name_substrings
= dn
.split(",")
216 del name_substrings
[0]
218 parent_dn
= ",".join(name_substrings
)
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
224 def _repl_send_request(self
, get_anc
=False, get_tgt
=False):
226 Sends a GetNCChanges request for the next block of replication data.
229 # we're just trying to mimic regular client behaviour here, so just
230 # use the highwatermark in the last response we received
232 highwatermark
= self
.last_ctr
.new_highwatermark
233 uptodateness_vector
= self
.last_ctr
.uptodateness_vector
235 # this is the first replication chunk
237 uptodateness_vector
= None
239 # Ask for the next block of replication data
240 replica_flags
= drsuapi
.DRSUAPI_DRS_WRIT_REP
244 replica_flags |
= drsuapi
.DRSUAPI_DRS_GET_ANC
245 self
.used_get_anc
= True
248 more_flags
= drsuapi
.DRSUAPI_DRS_GET_TGT
249 self
.used_get_tgt
= True
251 # return the response from the DC
252 return self
._get
_replication
(replica_flags
,
253 max_objects
=self
.max_objects
,
254 highwatermark
=highwatermark
,
255 uptodateness_vector
=uptodateness_vector
,
257 more_flags
=more_flags
)
259 def repl_get_next(self
, get_anc
=False, get_tgt
=False, assert_links
=False):
261 Requests the next block of replication data. This tries to simulate
262 client behaviour - if we receive a replicated object that we don't know
263 the parent of, then re-request the block with the GET_ANC flag set.
264 If we don't know the target object for a linked attribute, then
265 re-request with GET_TGT.
268 # send a request to the DC and get the response
269 ctr6
= self
._repl
_send
_request
(get_anc
=get_anc
, get_tgt
=get_tgt
)
271 # extract the object DNs and their GUIDs from the response
272 rxd_dn_list
= self
._get
_ctr
6_dn
_list
(ctr6
)
273 rxd_guid_list
= self
._get
_ctr
6_object
_guids
(ctr6
)
275 # we'll add new objects as we discover them, so take a copy of the
276 # ones we already know about, so we can modify these lists safely
277 known_objects
= self
.rxd_dn_list
[:]
278 known_guids
= self
.rxd_guids
[:]
280 # check that we know the parent for every object received
281 for i
in range(0, len(rxd_dn_list
)):
284 guid
= rxd_guid_list
[i
]
286 if self
.is_parent_known(dn
, known_objects
):
288 # the new DN is now known so add it to the list.
289 # It may be the parent of another child in this block
290 known_objects
.append(dn
)
291 known_guids
.append(guid
)
293 # If we've already set the GET_ANC flag then it should mean
294 # we receive the parents before the child
295 self
.assertFalse(get_anc
, "Unknown parent for object %s" % dn
)
297 print("Unknown parent for %s - try GET_ANC" % dn
)
299 # try the same thing again with the GET_ANC flag set this time
300 return self
.repl_get_next(get_anc
=True, get_tgt
=get_tgt
,
301 assert_links
=assert_links
)
303 # check we know about references to any objects in the linked attrs
304 received_links
= self
._get
_ctr
6_links
(ctr6
)
306 # This is so that older versions of Samba fail - we want the links to
307 # be sent roughly with the objects, rather than getting all links at
310 self
.assertTrue(len(received_links
) > 0,
311 "Links were expected in the GetNCChanges response")
313 for link
in received_links
:
315 # skip any links that aren't part of the test
316 if self
.ou
not in link
.targetDN
:
319 # check the source object is known (Windows can actually send links
320 # where we don't know the source object yet). Samba shouldn't ever
321 # hit this case because it gets the links based on the source
322 if link
.identifier
not in known_guids
:
324 # If we've already set the GET_ANC flag then it should mean
325 # this case doesn't happen
326 self
.assertFalse(get_anc
, "Unknown source object for GUID %s"
329 print("Unknown source GUID %s - try GET_ANC" % link
.identifier
)
331 # try the same thing again with the GET_ANC flag set this time
332 return self
.repl_get_next(get_anc
=True, get_tgt
=get_tgt
,
333 assert_links
=assert_links
)
335 # check we know the target object
336 if link
.targetGUID
not in known_guids
:
338 # If we've already set the GET_TGT flag then we should have
339 # already received any objects we need to know about
340 self
.assertFalse(get_tgt
, "Unknown linked target for object %s"
343 print("Unknown target for %s - try GET_TGT" % link
.targetDN
)
345 # try the same thing again with the GET_TGT flag set this time
346 return self
.repl_get_next(get_anc
=get_anc
, get_tgt
=True,
347 assert_links
=assert_links
)
349 # store the last successful result so we know what HWM to request next
352 # store the objects, GUIDs, and links we received
353 self
.rxd_dn_list
+= self
._get
_ctr
6_dn
_list
(ctr6
)
354 self
.rxd_links
+= self
._get
_ctr
6_links
(ctr6
)
355 self
.rxd_guids
+= self
._get
_ctr
6_object
_guids
(ctr6
)
359 def replication_complete(self
):
360 """Returns True if the current/last replication cycle is complete"""
362 if self
.last_ctr
is None or self
.last_ctr
.more_data
:
367 def test_repl_integrity_get_anc(self
):
369 Modify the parent objects being replicated while the replication is
370 still in progress (using GET_ANC) and check that no object loss occurs.
373 # Note that GET_ANC behaviour varies between Windows and Samba.
374 # On Samba GET_ANC results in the replication restarting from the very
375 # beginning. After that, Samba remembers GET_ANC and also sends the
376 # parents in subsequent requests (regardless of whether GET_ANC is
377 # specified in the later request).
378 # Windows only sends the parents if GET_ANC was specified in the last
379 # request. It will also resend a parent, even if it's already sent the
380 # parent in a previous response (whereas Samba doesn't).
382 # Create a small block of 50 parents, each with 2 children (A and B)
383 # This is so that we receive some children in the first block, so we
384 # can resend with GET_ANC before we learn too many parents
386 expected_dn_list
= self
.create_object_range(0, 50, prefix
="parent",
388 parent_list
=parent_dn_list
)
390 # create the remaining parents and children
391 expected_dn_list
+= self
.create_object_range(50, 150, prefix
="parent",
393 parent_list
=parent_dn_list
)
395 # We've now got objects in the following order:
396 # [50 parents][100 children][100 parents][200 children]
398 # Modify the first parent so that it's now ordered last by USN
399 # This means we set the GET_ANC flag pretty much straight away
400 # because we receive the first child before the first parent
401 self
.modify_object(parent_dn_list
[0], "displayName", "OU0")
403 # modify a later block of parents so they also get reordered
404 for x
in range(50, 100):
405 self
.modify_object(parent_dn_list
[x
], "displayName", "OU%d" % x
)
407 # Get the first block of objects - this should resend the request with
408 # GET_ANC set because we won't know about the first child's parent.
409 # On samba GET_ANC essentially starts the sync from scratch again, so
410 # we get this over with early before we learn too many parents
413 # modify the last chunk of parents. They should now have a USN higher
414 # than the highwater-mark for the replication cycle
415 for x
in range(100, 150):
416 self
.modify_object(parent_dn_list
[x
], "displayName", "OU%d" % x
)
418 # Get the remaining blocks of data - this will resend the request with
419 # GET_ANC if it encounters an object it doesn't have the parent for.
420 while not self
.replication_complete():
423 # The way the test objects have been created should force
424 # self.repl_get_next() to use the GET_ANC flag. If this doesn't
425 # actually happen, then the test isn't doing its job properly
426 self
.assertTrue(self
.used_get_anc
,
427 "Test didn't use the GET_ANC flag as expected")
429 # Check we get all the objects we're expecting
430 self
.assert_expected_data(expected_dn_list
)
432 def assert_expected_links(self
, objects_with_links
, link_attr
="managedBy",
435 Asserts that a GetNCChanges response contains any expected links
436 for the objects it contains.
438 received_links
= self
.rxd_links
440 if num_expected
is None:
441 num_expected
= len(objects_with_links
)
443 self
.assertTrue(len(received_links
) == num_expected
,
444 "Received %d links but expected %d"
445 % (len(received_links
), num_expected
))
447 for dn
in objects_with_links
:
448 self
.assert_object_has_link(dn
, link_attr
, received_links
)
450 def assert_object_has_link(self
, dn
, link_attr
, received_links
):
452 Queries the object in the DB and asserts there is a link in the
453 GetNCChanges response that matches.
456 # Look up the link attribute in the DB
457 # The extended_dn option will dump the GUID info for the link
458 # attribute (as a hex blob)
459 res
= self
.test_ldb_dc
.search(ldb
.Dn(self
.test_ldb_dc
, dn
),
461 controls
=['extended_dn:1:0'],
462 scope
=ldb
.SCOPE_BASE
)
464 # We didn't find the expected link attribute in the DB for the object.
465 # Something has gone wrong somewhere...
466 self
.assertTrue(link_attr
in res
[0],
467 "%s in DB doesn't have attribute %s" % (dn
, link_attr
))
469 # find the received link in the list and assert that the target and
470 # source GUIDs match what's in the DB
471 for val
in [str(val
) for val
in res
[0][link_attr
]]:
472 # Work out the expected source and target GUIDs for the DB link
473 target_dn
= ldb
.Dn(self
.test_ldb_dc
, val
)
474 targetGUID_blob
= target_dn
.get_extended_component("GUID")
475 sourceGUID_blob
= res
[0].dn
.get_extended_component("GUID")
479 for link
in received_links
:
480 if link
.selfGUID_blob
== sourceGUID_blob
and \
481 link
.targetGUID_blob
== targetGUID_blob
:
486 print("Link %s --> %s" % (dn
[:25], link
.targetDN
[:25]))
489 self
.assertTrue(found
,
490 "Did not receive expected link for DN %s" % dn
)
492 def test_repl_get_tgt(self
):
494 Creates a scenario where we should receive the linked attribute before
495 we know about the target object, and therefore need to use GET_TGT.
496 Note: Samba currently avoids this problem by sending all its links last
499 # create the test objects
500 reportees
= self
.create_object_range(0, 100, prefix
="reportee")
501 managers
= self
.create_object_range(0, 100, prefix
="manager")
502 all_objects
= managers
+ reportees
503 expected_links
= reportees
505 # add a link attribute to each reportee object that points to the
506 # corresponding manager object as the target
507 for i
in range(0, 100):
508 self
.modify_object(reportees
[i
], "managedBy", managers
[i
])
510 # touch the managers (the link-target objects) again to make sure the
511 # reportees (link source objects) get returned first by the replication
512 for i
in range(0, 100):
513 self
.modify_object(managers
[i
], "displayName", "OU%d" % i
)
515 links_expected
= True
517 # Get all the replication data - this code should resend the requests
519 while not self
.replication_complete():
521 # get the next block of replication data (this sets GET_TGT
523 self
.repl_get_next(assert_links
=links_expected
)
524 links_expected
= len(self
.rxd_links
) < len(expected_links
)
526 # The way the test objects have been created should force
527 # self.repl_get_next() to use the GET_TGT flag. If this doesn't
528 # actually happen, then the test isn't doing its job properly
529 self
.assertTrue(self
.used_get_tgt
,
530 "Test didn't use the GET_TGT flag as expected")
532 # Check we get all the objects we're expecting
533 self
.assert_expected_data(all_objects
)
535 # Check we received links for all the reportees
536 self
.assert_expected_links(expected_links
)
538 def test_repl_get_tgt_chain(self
):
540 Tests the behaviour of GET_TGT with a more complicated scenario.
541 Here we create a chain of objects linked together, so if we follow
542 the link target, then we'd traverse ~200 objects each time.
545 # create the test objects
546 objectsA
= self
.create_object_range(0, 100, prefix
="AAA")
547 objectsB
= self
.create_object_range(0, 100, prefix
="BBB")
548 objectsC
= self
.create_object_range(0, 100, prefix
="CCC")
550 # create a complex set of object links:
551 # A0-->B0-->C1-->B2-->C3-->B4-->and so on...
552 # Basically each object-A should link to a circular chain of 200 B/C
553 # objects. We create the links in separate chunks here, as it makes it
554 # clearer what happens with the USN (links on Windows have their own
555 # USN, so this approach means the A->B/B->C links aren't interleaved)
556 for i
in range(0, 100):
557 self
.modify_object(objectsA
[i
], "managedBy", objectsB
[i
])
559 for i
in range(0, 100):
560 self
.modify_object(objectsB
[i
], "managedBy",
561 objectsC
[(i
+ 1) % 100])
563 for i
in range(0, 100):
564 self
.modify_object(objectsC
[i
], "managedBy",
565 objectsB
[(i
+ 1) % 100])
567 all_objects
= objectsA
+ objectsB
+ objectsC
568 expected_links
= all_objects
570 # the default order the objects now get returned in should be:
571 # [A0-A99][B0-B99][C0-C99]
573 links_expected
= True
575 # Get all the replication data - this code should resend the requests
577 while not self
.replication_complete():
579 # get the next block of replication data (this sets GET_TGT
581 self
.repl_get_next(assert_links
=links_expected
)
582 links_expected
= len(self
.rxd_links
) < len(expected_links
)
584 # The way the test objects have been created should force
585 # self.repl_get_next() to use the GET_TGT flag. If this doesn't
586 # actually happen, then the test isn't doing its job properly
587 self
.assertTrue(self
.used_get_tgt
,
588 "Test didn't use the GET_TGT flag as expected")
590 # Check we get all the objects we're expecting
591 self
.assert_expected_data(all_objects
)
593 # Check we received links for all the reportees
594 self
.assert_expected_links(expected_links
)
596 def test_repl_integrity_link_attr(self
):
598 Tests adding links to new objects while a replication is in progress.
601 # create some source objects for the linked attributes, sandwiched
602 # between 2 blocks of filler objects
603 filler
= self
.create_object_range(0, 100, prefix
="filler")
604 reportees
= self
.create_object_range(0, 100, prefix
="reportee")
605 filler
+= self
.create_object_range(100, 200, prefix
="filler")
607 # Start the replication and get the first block of filler objects
608 # (We're being mean here and setting the GET_TGT flag right from the
609 # start. On earlier Samba versions, if the client encountered an
610 # unknown target object and retried with GET_TGT, it would restart the
611 # replication cycle from scratch, which avoids the problem).
612 self
.repl_get_next(get_tgt
=True)
614 # create the target objects and add the links. These objects should be
615 # outside the scope of the Samba replication cycle, but the links
616 # should still get sent with the source object
617 managers
= self
.create_object_range(0, 100, prefix
="manager")
619 for i
in range(0, 100):
620 self
.modify_object(reportees
[i
], "managedBy", managers
[i
])
622 expected_objects
= managers
+ reportees
+ filler
623 expected_links
= reportees
625 # complete the replication
626 while not self
.replication_complete():
627 self
.repl_get_next(get_tgt
=True)
629 # If we didn't receive the most recently created objects in the last
630 # replication cycle, then kick off another replication to get them
631 if len(self
.rxd_dn_list
) < len(expected_objects
):
634 while not self
.replication_complete():
637 # Check we get all the objects we're expecting
638 self
.assert_expected_data(expected_objects
)
640 # Check we received links for all the parents
641 self
.assert_expected_links(expected_links
)
643 def test_repl_get_anc_link_attr(self
):
645 A basic GET_ANC test where the parents have linked attributes
648 # Create a block of 100 parents and 100 children
650 expected_dn_list
= self
.create_object_range(0, 100, prefix
="parent",
652 parent_list
=parent_dn_list
)
654 # Add links from the parents to the children
655 for x
in range(0, 100):
656 self
.modify_object(parent_dn_list
[x
], "managedBy",
657 expected_dn_list
[x
+ 100])
659 # add some filler objects at the end. This allows us to easily see
660 # which chunk the links get sent in
661 expected_dn_list
+= self
.create_object_range(0, 100, prefix
="filler")
663 # We've now got objects in the following order:
664 # [100 x children][100 x parents][100 x filler]
666 # Get the replication data - because the block of children come first,
667 # this should retry the request with GET_ANC
668 while not self
.replication_complete():
671 self
.assertTrue(self
.used_get_anc
,
672 "Test didn't use the GET_ANC flag as expected")
674 # Check we get all the objects we're expecting
675 self
.assert_expected_data(expected_dn_list
)
677 # Check we received links for all the parents
678 self
.assert_expected_links(parent_dn_list
)
680 def test_repl_get_tgt_and_anc(self
):
682 Check we can resolve an unknown ancestor when fetching the link target,
683 i.e. tests using GET_TGT and GET_ANC in combination
686 # Create some parent/child objects (the child will be the link target)
688 all_objects
= self
.create_object_range(0, 100, prefix
="parent",
692 children
= [item
for item
in all_objects
if item
not in parents
]
694 # create the link source objects and link them to the child/target
695 la_sources
= self
.create_object_range(0, 100, prefix
="la_src")
696 all_objects
+= la_sources
698 for i
in range(0, 100):
699 self
.modify_object(la_sources
[i
], "managedBy", children
[i
])
701 expected_links
= la_sources
703 # modify the children/targets so they come after the link source
704 for x
in range(0, 100):
705 self
.modify_object(children
[x
], "displayName", "OU%d" % x
)
707 # modify the parents, so they now come last in the replication
708 for x
in range(0, 100):
709 self
.modify_object(parents
[x
], "displayName", "OU%d" % x
)
711 # We've now got objects in the following order:
712 # [100 la_source][100 la_target][100 parents (of la_target)]
714 links_expected
= True
716 # Get all the replication data - this code should resend the requests
717 # with GET_TGT and GET_ANC
718 while not self
.replication_complete():
720 # get the next block of replication data (this sets
722 self
.repl_get_next(assert_links
=links_expected
)
723 links_expected
= len(self
.rxd_links
) < len(expected_links
)
725 # The way the test objects have been created should force
726 # self.repl_get_next() to use the GET_TGT/GET_ANC flags. If this
727 # doesn't actually happen, then the test isn't doing its job properly
728 self
.assertTrue(self
.used_get_tgt
,
729 "Test didn't use the GET_TGT flag as expected")
730 self
.assertTrue(self
.used_get_anc
,
731 "Test didn't use the GET_ANC flag as expected")
733 # Check we get all the objects we're expecting
734 self
.assert_expected_data(all_objects
)
736 # Check we received links for all the link sources
737 self
.assert_expected_links(expected_links
)
739 # Second part of test. Add some extra objects and kick off another
740 # replication. The test code will use the HWM from the last replication
741 # so we'll only receive the objects we modify below
742 self
.start_new_repl_cycle()
744 # add an extra level of grandchildren that hang off a child
745 # that got created last time
746 new_parent
= "OU=test_new_parent,%s" % children
[0]
747 self
.add_object(new_parent
)
750 for x
in range(0, 50):
751 dn
= "OU=test_new_la_tgt%d,%s" % (x
, new_parent
)
753 new_children
.append(dn
)
755 # replace half of the links to point to the new children
756 for x
in range(0, 50):
757 self
.delete_attribute(la_sources
[x
], "managedBy", children
[x
])
758 self
.modify_object(la_sources
[x
], "managedBy", new_children
[x
])
760 # add some filler objects to fill up the 1st chunk
761 filler
= self
.create_object_range(0, 100, prefix
="filler")
763 # modify the new children/targets so they come after the link source
764 for x
in range(0, 50):
765 self
.modify_object(new_children
[x
], "displayName", "OU-%d" % x
)
767 # modify the parent, so it now comes last in the replication
768 self
.modify_object(new_parent
, "displayName", "OU%d" % x
)
770 # We should now get the modified objects in the following order:
771 # [50 links (x 2)][100 filler][50 new children][new parent]
772 # Note that the link sources aren't actually sent (their new linked
773 # attributes are sent, but apart from that, nothing has changed)
774 all_objects
= filler
+ new_children
+ [new_parent
]
775 expected_links
= la_sources
[:50]
777 links_expected
= True
779 while not self
.replication_complete():
780 self
.repl_get_next(assert_links
=links_expected
)
781 links_expected
= len(self
.rxd_links
) < len(expected_links
)
783 self
.assertTrue(self
.used_get_tgt
,
784 "Test didn't use the GET_TGT flag as expected")
785 self
.assertTrue(self
.used_get_anc
,
786 "Test didn't use the GET_ANC flag as expected")
788 # Check we get all the objects we're expecting
789 self
.assert_expected_data(all_objects
)
791 # Check we received links (50 deleted links and 50 new)
792 self
.assert_expected_links(expected_links
, num_expected
=100)
794 def _repl_integrity_obj_deletion(self
, delete_link_source
=True):
796 Tests deleting link objects while a replication is in progress.
799 # create some objects and link them together, with some filler
800 # object in between the link sources
801 la_sources
= self
.create_object_range(0, 100, prefix
="la_source")
802 la_targets
= self
.create_object_range(0, 100, prefix
="la_targets")
804 for i
in range(0, 50):
805 self
.modify_object(la_sources
[i
], "managedBy", la_targets
[i
])
807 filler
= self
.create_object_range(0, 100, prefix
="filler")
809 for i
in range(50, 100):
810 self
.modify_object(la_sources
[i
], "managedBy", la_targets
[i
])
812 # touch the targets so that the sources get replicated first
813 for i
in range(0, 100):
814 self
.modify_object(la_targets
[i
], "displayName", "OU%d" % i
)
816 # objects should now be in the following USN order:
817 # [50 la_source][100 filler][50 la_source][100 la_target]
819 # Get the first block containing 50 link sources
822 # delete either the link targets or link source objects
823 if delete_link_source
:
824 objects_to_delete
= la_sources
825 # in GET_TGT testenvs we only receive the first 50 source objects
826 expected_objects
= la_sources
[:50] + la_targets
+ filler
828 objects_to_delete
= la_targets
829 expected_objects
= la_sources
+ filler
831 for obj
in objects_to_delete
:
832 self
.ldb_dc2
.delete(obj
)
834 # complete the replication
835 while not self
.replication_complete():
838 # Check we get all the objects we're expecting
839 self
.assert_expected_data(expected_objects
)
841 # we can't use assert_expected_links() here because it tries to check
842 # against the deleted objects on the DC. (Although we receive some
843 # links from the first block processed, the Samba client should end up
844 # deleting these, as the source/target object involved is deleted)
845 self
.assertTrue(len(self
.rxd_links
) == 50,
846 "Expected 50 links, not %d" % len(self
.rxd_links
))
848 def test_repl_integrity_src_obj_deletion(self
):
849 self
._repl
_integrity
_obj
_deletion
(delete_link_source
=True)
851 def test_repl_integrity_tgt_obj_deletion(self
):
852 self
._repl
_integrity
_obj
_deletion
(delete_link_source
=False)
854 def restore_deleted_object(self
, guid
, new_dn
):
855 """Re-animates a deleted object"""
857 guid_str
= self
._GUID
_string
(guid
)
858 res
= self
.test_ldb_dc
.search(base
="<GUID=%s>" % guid_str
,
860 controls
=['show_deleted:1'],
861 scope
=ldb
.SCOPE_BASE
)
867 msg
["isDeleted"] = ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
,
869 msg
["distinguishedName"] = ldb
.MessageElement([new_dn
],
870 ldb
.FLAG_MOD_REPLACE
,
872 self
.test_ldb_dc
.modify(msg
, ["show_deleted:1"])
874 def sync_DCs(self
, nc_dn
=None):
875 # make sure DC1 has all the changes we've made to DC2
876 self
._net
_drs
_replicate
(DC
=self
.dnsname_dc1
, fromDC
=self
.dnsname_dc2
,
879 def get_object_guid(self
, dn
):
880 res
= self
.test_ldb_dc
.search(base
=dn
, attrs
=["objectGUID"],
881 scope
=ldb
.SCOPE_BASE
)
882 return res
[0]['objectGUID'][0]
884 def set_dc_connection(self
, conn
):
886 Switches over the connection state info that the underlying drs_base
887 class uses so that we replicate with a different DC.
889 self
.default_hwm
= conn
.default_hwm
890 self
.default_utdv
= conn
.default_utdv
892 self
.drs_handle
= conn
.drs_handle
893 self
.set_test_ldb_dc(conn
.ldb_dc
)
895 def assert_DCs_replication_is_consistent(self
, peer_conn
, all_objects
,
898 Replicates against both the primary and secondary DCs in the testenv
899 and checks that both return the expected results.
901 print("Checking replication against primary test DC...")
903 # get the replication data from the test DC first
904 while not self
.replication_complete():
907 # Check we get all the objects and links we're expecting
908 self
.assert_expected_data(all_objects
)
909 self
.assert_expected_links(expected_links
)
911 # switch over the DC state info so we now talk to the peer DC
912 self
.set_dc_connection(peer_conn
)
913 self
.init_test_state()
915 print("Checking replication against secondary test DC...")
917 # check that we get the same information from the 2nd DC
918 while not self
.replication_complete():
921 self
.assert_expected_data(all_objects
)
922 self
.assert_expected_links(expected_links
)
924 # switch back to using the default connection
925 self
.set_dc_connection(self
.default_conn
)
927 def test_repl_integrity_obj_reanimation(self
):
929 Checks receiving links for a re-animated object doesn't lose links.
930 We test this against the peer DC to make sure it doesn't drop links.
933 # This test is a little different in that we're particularly interested
934 # in exercising the replmd client code on the second DC.
935 # First, make sure the peer DC has the base OU, then connect to it (so
936 # we store its initial HWM)
938 peer_conn
= DcConnection(self
, self
.ldb_dc1
, self
.dnsname_dc1
)
940 # create the link source/target objects
941 la_sources
= self
.create_object_range(0, 100, prefix
="la_src")
942 la_targets
= self
.create_object_range(0, 100, prefix
="la_tgt")
944 # store the target object's GUIDs (we need to know these to
948 for dn
in la_targets
:
949 target_guids
.append(self
.get_object_guid(dn
))
951 # delete the link target
952 for x
in range(0, 100):
953 self
.ldb_dc2
.delete(la_targets
[x
])
955 # sync the DCs, then disable replication. We want the peer DC to get
956 # all the following changes in a single replication cycle
958 self
._disable
_all
_repl
(self
.dnsname_dc2
)
960 # restore the target objects for the linked attributes again
961 for x
in range(0, 100):
962 self
.restore_deleted_object(target_guids
[x
], la_targets
[x
])
965 for x
in range(0, 100):
966 self
.modify_object(la_sources
[x
], "managedBy", la_targets
[x
])
968 # create some additional filler objects
969 filler
= self
.create_object_range(0, 100, prefix
="filler")
971 # modify the targets so they now come last
972 for x
in range(0, 100):
973 self
.modify_object(la_targets
[x
], "displayName", "OU-%d" % x
)
975 # the objects should now be sent in the following order:
976 # [la sources + links][filler][la targets]
977 all_objects
= la_sources
+ la_targets
+ filler
978 expected_links
= la_sources
980 # Enable replication again make sure the 2 DCs are back in sync
981 self
._enable
_all
_repl
(self
.dnsname_dc2
)
984 # Get the replication data from each DC in turn.
985 # Check that both give us all the objects and links we're expecting,
986 # i.e. no links were lost
987 self
.assert_DCs_replication_is_consistent(peer_conn
, all_objects
,
990 def _test_repl_integrity_cross_partition_links(self
, get_tgt
=False):
992 Checks that a cross-partition link to an unknown target object does
993 not result in missing links.
996 # check the peer DC is up-to-date, then connect (storing its HWM)
998 peer_conn
= DcConnection(self
, self
.ldb_dc1
, self
.dnsname_dc1
)
1000 # stop replication so the peer gets the following objects in one go
1001 self
._disable
_all
_repl
(self
.dnsname_dc2
)
1003 # optionally force the client-side to use GET_TGT locally, by adding a
1004 # one-way link to a missing/deleted target object
1006 missing_target
= "OU=missing_tgt,%s" % self
.ou
1007 self
.add_object(missing_target
)
1008 get_tgt_source
= "CN=get_tgt_src,%s" % self
.ou
1009 self
.add_object(get_tgt_source
,
1010 objectclass
="msExchConfigurationContainer")
1011 self
.modify_object(get_tgt_source
, "addressBookRoots2",
1013 self
.test_ldb_dc
.delete(missing_target
)
1015 # create a link source object in the main NC
1016 la_source
= "OU=cross_nc_src,%s" % self
.ou
1017 self
.add_object(la_source
)
1019 # create the link target (a server object) in the config NC
1020 sites_dn
= "CN=Sites,%s" % self
.config_dn
1021 servers_dn
= "CN=Servers,CN=Default-First-Site-Name,%s" % sites_dn
1022 rand
= random
.randint(1, 10000000)
1023 la_target
= "CN=getncchanges-%d,%s" % (rand
, servers_dn
)
1024 self
.add_object(la_target
, objectclass
="server")
1026 # add a cross-partition link between the two
1027 self
.modify_object(la_source
, "managedBy", la_target
)
1029 # First, sync to the peer the NC containing the link source object
1032 # Now, before the peer has received the partition containing the target
1033 # object, try replicating from the peer. It will only know about half
1034 # of the link at this point, but it should be a valid scenario
1035 self
.set_dc_connection(peer_conn
)
1037 while not self
.replication_complete():
1038 # pretend we've received other link targets out of order and that's
1039 # forced us to use GET_TGT. This checks the peer doesn't fail
1040 # trying to fetch a cross-partition target object that doesn't
1042 self
.repl_get_next(get_tgt
=True)
1044 self
.set_dc_connection(self
.default_conn
)
1046 # delete the GET_TGT test object. We're not interested in asserting its
1047 # links - it was just there to make the client use GET_TGT (and it
1048 # creates an inconsistency because one DC correctly ignores the link,
1049 # because it points to a deleted object)
1051 self
.test_ldb_dc
.delete(get_tgt_source
)
1053 self
.init_test_state()
1055 # Now sync across the partition containing the link target object
1056 self
.sync_DCs(nc_dn
=self
.config_dn
)
1057 self
._enable
_all
_repl
(self
.dnsname_dc2
)
1059 # Get the replication data from each DC in turn.
1060 # Check that both return the cross-partition link (note we're not
1061 # checking the config domain NC here for simplicity)
1062 self
.assert_DCs_replication_is_consistent(peer_conn
,
1063 all_objects
=[la_source
],
1064 expected_links
=[la_source
])
1066 # the cross-partition linked attribute has a missing backlink. Check
1067 # that we can still delete it successfully
1068 self
.delete_attribute(la_source
, "managedBy", la_target
)
1071 res
= self
.test_ldb_dc
.search(ldb
.Dn(self
.ldb_dc1
, la_source
),
1072 attrs
=["managedBy"],
1073 controls
=['extended_dn:1:0'],
1074 scope
=ldb
.SCOPE_BASE
)
1075 self
.assertFalse("managedBy" in res
[0],
1076 "%s in DB still has managedBy attribute" % la_source
)
1077 res
= self
.test_ldb_dc
.search(ldb
.Dn(self
.ldb_dc2
, la_source
),
1078 attrs
=["managedBy"],
1079 controls
=['extended_dn:1:0'],
1080 scope
=ldb
.SCOPE_BASE
)
1081 self
.assertFalse("managedBy" in res
[0],
1082 "%s in DB still has managedBy attribute" % la_source
)
1084 # Check receiving a cross-partition link to a deleted target.
1085 # Delete the target and make sure the deletion is sync'd between DCs
1086 target_guid
= self
.get_object_guid(la_target
)
1087 self
.test_ldb_dc
.delete(la_target
)
1088 self
.sync_DCs(nc_dn
=self
.config_dn
)
1089 self
._disable
_all
_repl
(self
.dnsname_dc2
)
1091 # re-animate the target
1092 self
.restore_deleted_object(target_guid
, la_target
)
1093 self
.modify_object(la_source
, "managedBy", la_target
)
1095 # now sync the link - because the target is in another partition, the
1096 # peer DC receives a link for a deleted target, which it should accept
1098 res
= self
.test_ldb_dc
.search(ldb
.Dn(self
.ldb_dc1
, la_source
),
1099 attrs
=["managedBy"],
1100 controls
=['extended_dn:1:0'],
1101 scope
=ldb
.SCOPE_BASE
)
1102 self
.assertTrue("managedBy" in res
[0],
1103 "%s in DB missing managedBy attribute" % la_source
)
1105 # cleanup the server object we created in the Configuration partition
1106 self
.test_ldb_dc
.delete(la_target
)
1107 self
._enable
_all
_repl
(self
.dnsname_dc2
)
1109 def test_repl_integrity_cross_partition_links(self
):
1110 self
._test
_repl
_integrity
_cross
_partition
_links
(get_tgt
=False)
1112 def test_repl_integrity_cross_partition_links_with_tgt(self
):
1113 self
._test
_repl
_integrity
_cross
_partition
_links
(get_tgt
=True)
1115 def test_repl_get_tgt_multivalued_links(self
):
1116 """Tests replication with multi-valued link attributes."""
1118 # create the target/source objects and link them together
1119 la_targets
= self
.create_object_range(0, 500, prefix
="la_tgt")
1120 la_source
= "CN=la_src,%s" % self
.ou
1121 self
.add_object(la_source
, objectclass
="msExchConfigurationContainer")
1123 for tgt
in la_targets
:
1124 self
.modify_object(la_source
, "addressBookRoots2", tgt
)
1126 filler
= self
.create_object_range(0, 100, prefix
="filler")
1128 # We should receive the objects/links in the following order:
1129 # [500 targets + 1 source][500 links][100 filler]
1130 expected_objects
= la_targets
+ [la_source
] + filler
1131 link_only_chunk
= False
1133 # First do the replication without needing GET_TGT
1134 while not self
.replication_complete():
1135 ctr6
= self
.repl_get_next()
1137 if ctr6
.object_count
== 0 and ctr6
.linked_attributes_count
!= 0:
1138 link_only_chunk
= True
1140 # we should receive one chunk that contains only links
1141 self
.assertTrue(link_only_chunk
,
1142 "Expected to receive a chunk containing only links")
1144 # check we received all the expected objects/links
1145 self
.assert_expected_data(expected_objects
)
1146 self
.assert_expected_links([la_source
], link_attr
="addressBookRoots2",
1149 # Do the replication again, forcing the use of GET_TGT this time
1150 self
.init_test_state()
1152 for x
in range(0, 500):
1153 self
.modify_object(la_targets
[x
], "displayName", "OU-%d" % x
)
1155 # The objects/links should get sent in the following order:
1156 # [1 source][500 targets][500 links][100 filler]
1158 while not self
.replication_complete():
1159 ctr6
= self
.repl_get_next()
1161 self
.assertTrue(self
.used_get_tgt
,
1162 "Test didn't use the GET_TGT flag as expected")
1164 # check we received all the expected objects/links
1165 self
.assert_expected_data(expected_objects
)
1166 self
.assert_expected_links([la_source
], link_attr
="addressBookRoots2",
1170 def test_InvalidNC_DummyDN_InvalidGUID_full_repl(self
):
1171 """Test full replication on a totally invalid GUID fails with the right error code"""
1172 dc_guid_1
= self
.ldb_dc1
.get_invocation_id()
1173 drs
, drs_handle
= self
._ds
_bind
(self
.dnsname_dc1
, ip
=self
.url_dc1
)
1175 req8
= self
._exop
_req
8(dest_dsa
="9c637462-5b8c-4467-aef2-bdb1f57bc4ef",
1176 invocation_id
=dc_guid_1
,
1177 nc_dn_str
="DummyDN",
1178 nc_guid
=misc
.GUID("c2d2f745-1610-4e93-964b-d4ba73eb32f8"),
1179 exop
=drsuapi
.DRSUAPI_EXOP_NONE
,
1182 (drs
, drs_handle
) = self
._ds
_bind
(self
.dnsname_dc1
, ip
=self
.url_dc1
)
1184 (level
, ctr
) = drs
.DsGetNCChanges(drs_handle
, 8, req8
)
1185 except WERRORError
as e1
:
1186 (enum
, estr
) = e1
.args
1187 self
.assertEqual(enum
, werror
.WERR_DS_DRA_BAD_NC
)
1189 def test_DummyDN_valid_GUID_full_repl(self
):
1190 dc_guid_1
= self
.ldb_dc1
.get_invocation_id()
1191 drs
, drs_handle
= self
._ds
_bind
(self
.dnsname_dc1
, ip
=self
.url_dc1
)
1193 res
= self
.ldb_dc1
.search(base
=self
.base_dn
, scope
=SCOPE_BASE
,
1194 attrs
=["objectGUID"])
1196 guid
= misc
.GUID(res
[0]["objectGUID"][0])
1198 req8
= self
._exop
_req
8(dest_dsa
=None,
1199 invocation_id
=dc_guid_1
,
1200 nc_dn_str
="DummyDN",
1202 replica_flags
=drsuapi
.DRSUAPI_DRS_WRIT_REP |
1203 drsuapi
.DRSUAPI_DRS_GET_ANC
,
1204 exop
=drsuapi
.DRSUAPI_EXOP_NONE
,
1208 (level
, ctr
) = drs
.DsGetNCChanges(drs_handle
, 8, req8
)
1209 except WERRORError
as e1
:
1210 (enum
, estr
) = e1
.args
1211 self
.fail(f
"Failed to call GetNCChanges with DummyDN and a GUID: {estr}")
1213 # The NC should be the first object returned due to GET_ANC
1214 self
.assertEqual(ctr
.first_object
.object.identifier
.guid
, guid
)
1216 def _test_do_full_repl_no_overlap(self
, mix
=True, get_anc
=False):
1217 self
.default_hwm
= drsuapi
.DsReplicaHighWaterMark()
1219 # We set get_anc=True so we can assert the BASE DN will be the
1221 ctr6
= self
._repl
_send
_request
(get_anc
=get_anc
)
1222 guid_list_1
= self
._get
_ctr
6_object
_guids
(ctr6
)
1225 dc_guid_1
= self
.ldb_dc1
.get_invocation_id()
1227 req8
= self
._exop
_req
8(dest_dsa
=None,
1228 invocation_id
=dc_guid_1
,
1229 nc_dn_str
=self
.ldb_dc1
.get_default_basedn(),
1230 exop
=drsuapi
.DRSUAPI_EXOP_REPL_OBJ
)
1232 (level
, ctr_repl_obj
) = self
.drs
.DsGetNCChanges(self
.drs_handle
, 8, req8
)
1234 self
.assertEqual(ctr_repl_obj
.extended_ret
, drsuapi
.DRSUAPI_EXOP_ERR_SUCCESS
)
1236 repl_obj_guid_list
= self
._get
_ctr
6_object
_guids
(ctr_repl_obj
)
1238 self
.assertEqual(len(repl_obj_guid_list
), 1)
1240 # This should be the first object in the main replication due
1241 # to get_anc=True above in one case, and a rule that the NC must be first regardless otherwise
1242 self
.assertEqual(repl_obj_guid_list
[0], guid_list_1
[0])
1244 self
.last_ctr
= ctr6
1245 ctr6
= self
._repl
_send
_request
(get_anc
=True)
1246 guid_list_2
= self
._get
_ctr
6_object
_guids
(ctr6
)
1248 self
.assertNotEqual(guid_list_1
, guid_list_2
)
1250 def test_do_full_repl_no_overlap_get_anc(self
):
1252 Make sure that a full replication on an nc succeeds to the goal despite needing multiple passes
1254 self
._test
_do
_full
_repl
_no
_overlap
(mix
=False, get_anc
=True)
1256 def test_do_full_repl_no_overlap(self
):
1258 Make sure that a full replication on an nc succeeds to the goal despite needing multiple passes
1260 self
._test
_do
_full
_repl
_no
_overlap
(mix
=False)
1262 def test_do_full_repl_mix_no_overlap(self
):
1264 Make sure that a full replication on an nc succeeds to the goal despite needing multiple passes
1266 Assert this is true even if we do a REPL_OBJ in between the replications
1269 self
._test
_do
_full
_repl
_no
_overlap
(mix
=True)
1271 def nc_change(self
):
1272 old_base_msg
= self
.default_conn
.ldb_dc
.search(base
=self
.base_dn
,
1274 attrs
=["oEMInformation"])
1275 rec_cleanup
= {"dn": self
.base_dn
,
1276 "oEMInformation": old_base_msg
[0]["oEMInformation"][0]}
1277 m_cleanup
= ldb
.Message
.from_dict(self
.default_conn
.ldb_dc
,
1279 ldb
.FLAG_MOD_REPLACE
)
1281 self
.addCleanup(self
.default_conn
.ldb_dc
.modify
, m_cleanup
)
1283 rec
= {"dn": self
.base_dn
,
1284 "oEMInformation": f
"Tortured by Samba's getncchanges.py {self.id()} against {self.default_conn.dnsname_dc}"}
1285 m
= ldb
.Message
.from_dict(self
.default_conn
.ldb_dc
, rec
, ldb
.FLAG_MOD_REPLACE
)
1286 self
.default_conn
.ldb_dc
.modify(m
)
1288 def _test_repl_nc_is_first(self
, start_at_zero
=True, nc_change
=True, ou_change
=True, mid_change
=False):
1289 """Tests that the NC is always replicated first, but does not move the
1290 tmp_highest_usn at that point, just like 'early' GET_ANC objects.
1293 # create objects, twice more than the page size of 133
1294 objs
= self
.create_object_range(0, 300, prefix
="obj")
1300 # create even more objects
1301 objs
= self
.create_object_range(301, 450, prefix
="obj2")
1303 base_msg
= self
.default_conn
.ldb_dc
.search(base
=self
.base_dn
,
1305 attrs
=["uSNChanged",
1308 base_guid
= misc
.GUID(base_msg
[0]["objectGUID"][0])
1309 base_usn
= int(base_msg
[0]["uSNChanged"][0])
1312 # Make one more modification. We want to assert we have
1313 # caught up to the base DN, but Windows both promotes the NC
1314 # to the front and skips including it in the tmp_highest_usn,
1315 # so we make a later modification that will be to show we get
1317 rec
= {"dn": self
.ou
,
1319 m
= ldb
.Message
.from_dict(self
.default_conn
.ldb_dc
, rec
, ldb
.FLAG_MOD_REPLACE
)
1320 self
.default_conn
.ldb_dc
.modify(m
)
1322 ou_msg
= self
.default_conn
.ldb_dc
.search(base
=self
.ou
,
1324 attrs
=["uSNChanged",
1327 ou_guid
= misc
.GUID(ou_msg
[0]["objectGUID"][0])
1328 ou_usn
= int(ou_msg
[0]["uSNChanged"][0])
1330 # Check some predicates about USN ordering that the below tests will rely on
1331 if ou_change
and nc_change
:
1332 self
.assertGreater(ou_usn
, base_usn
)
1333 elif not ou_change
and nc_change
:
1334 self
.assertGreater(base_usn
, ou_usn
)
1336 ctr6
= self
.repl_get_next()
1338 guid_list_1
= self
._get
_ctr
6_object
_guids
(ctr6
)
1339 if nc_change
or start_at_zero
:
1340 self
.assertEqual(base_guid
, misc
.GUID(guid_list_1
[0]))
1341 self
.assertIn(str(base_guid
), guid_list_1
)
1342 self
.assertNotIn(str(base_guid
), guid_list_1
[1:])
1344 self
.assertNotEqual(base_guid
, misc
.GUID(guid_list_1
[0]))
1345 self
.assertNotIn(str(base_guid
), guid_list_1
)
1347 self
.assertTrue(ctr6
.more_data
)
1349 if not ou_change
and nc_change
:
1350 self
.assertLess(ctr6
.new_highwatermark
.tmp_highest_usn
, base_usn
)
1353 while not self
.replication_complete():
1355 last_tmp_highest_usn
= ctr6
.new_highwatermark
.tmp_highest_usn
1356 ctr6
= self
.repl_get_next()
1357 guid_list_2
= self
._get
_ctr
6_object
_guids
(ctr6
)
1358 if len(guid_list_2
) > 0:
1359 self
.assertNotEqual(last_tmp_highest_usn
, ctr6
.new_highwatermark
.tmp_highest_usn
)
1361 if (nc_change
or start_at_zero
) and base_usn
> last_tmp_highest_usn
:
1362 self
.assertEqual(base_guid
, misc
.GUID(guid_list_2
[0]),
1363 f
"pass={i} more_data={ctr6.more_data} base_usn={base_usn} tmp_highest_usn={ctr6.new_highwatermark.tmp_highest_usn} last_tmp_highest_usn={last_tmp_highest_usn}")
1364 self
.assertIn(str(base_guid
), guid_list_2
,
1365 f
"pass {i}·more_data={ctr6.more_data} base_usn={base_usn} tmp_highest_usn={ctr6.new_highwatermark.tmp_highest_usn} last_tmp_highest_usn={last_tmp_highest_usn}")
1367 self
.assertNotIn(str(base_guid
), guid_list_2
,
1368 f
"pass {i}·more_data={ctr6.more_data} base_usn={base_usn} tmp_highest_usn={ctr6.new_highwatermark.tmp_highest_usn} last_tmp_highest_usn={last_tmp_highest_usn}")
1371 # The modification to the base OU should be in the final chunk
1372 self
.assertIn(str(ou_guid
), guid_list_2
)
1373 self
.assertGreaterEqual(ctr6
.new_highwatermark
.highest_usn
,
1376 # Show that the NC root change does not show up in the
1377 # highest_usn. We either get the change before or after
1379 self
.assertNotEqual(ctr6
.new_highwatermark
.highest_usn
,
1381 self
.assertEqual(ctr6
.new_highwatermark
.highest_usn
,
1382 ctr6
.new_highwatermark
.tmp_highest_usn
)
1384 self
.assertFalse(ctr6
.more_data
)
1386 def test_repl_nc_is_first_start_zero_nc_change(self
):
1387 self
.default_hwm
= drsuapi
.DsReplicaHighWaterMark()
1388 self
._test
_repl
_nc
_is
_first
(start_at_zero
=True, nc_change
=True, ou_change
=True)
1390 def test_repl_nc_is_first_start_zero(self
):
1391 # Get the NC change in the middle of the replication stream, certainly not at the start or end
1393 self
.default_hwm
= drsuapi
.DsReplicaHighWaterMark()
1394 self
._test
_repl
_nc
_is
_first
(start_at_zero
=True, nc_change
=False, ou_change
=False)
1396 def test_repl_nc_is_first_mid(self
):
1397 # This is a modification of the next test, that Samba
1398 # will pass as it will always include the NC in the
1399 # tmp_highest_usn at the point where it belongs
1400 self
._test
_repl
_nc
_is
_first
(start_at_zero
=False,
1405 def test_repl_nc_is_first(self
):
1406 # This is a modification of the next test, that Samba
1407 # will pass as it will always include the NC in the
1408 # tmp_highest_usn at the point where it belongs
1409 self
._test
_repl
_nc
_is
_first
(start_at_zero
=False, nc_change
=True, ou_change
=True)
1411 def test_repl_nc_is_first_nc_change_only(self
):
1412 # This shows that the NC change is not reflected in the tmp_highest_usn
1413 self
._test
_repl
_nc
_is
_first
(start_at_zero
=False, nc_change
=True, ou_change
=False)
1415 def test_repl_nc_is_first_no_change(self
):
1416 # The NC should not be present in this replication
1417 self
._test
_repl
_nc
_is
_first
(start_at_zero
=False, nc_change
=False, ou_change
=False)
1420 class DrsReplicaSyncFakeAzureAdTests(DrsReplicaSyncIntegrityTestCase
):
1421 """This repeats all of DrsReplicaSyncIntegrityTestCase, but the client
1422 always sets highwatermark.reserved_usn = 0. This is what Azure AD
1423 / Entra ID Connect does.
1426 def modify_highwatermark(hwm
):
1428 hwm
.reserved_usn
= 0
1431 "test_repl_get_tgt",
1432 "test_repl_get_tgt_and_anc",
1433 "test_repl_get_tgt_chain",
1434 "test_do_full_repl_mix_no_overlap",
1435 "test_do_full_repl_no_overlap",
1436 "test_do_full_repl_no_overlap_get_anc",
1437 "test_DummyDN_valid_GUID_full_repl",
1438 "test_InvalidNC_DummyDN_InvalidGUID_full_repl",
1439 "test_repl_get_anc_link_attr",
1440 "test_repl_integrity_cross_partition_links",
1441 "test_repl_integrity_cross_partition_links_with_tgt",
1442 "test_repl_integrity_get_anc",
1443 "test_repl_integrity_obj_reanimation",
1444 "test_repl_integrity_src_obj_deletion",
1445 "test_repl_integrity_tgt_obj_deletion",
1446 "test_repl_nc_is_first",
1447 "test_repl_nc_is_first_mid",
1448 "test_repl_nc_is_first_nc_change_only", # xfail in parent
1449 "test_repl_nc_is_first_no_change",
1450 "test_repl_nc_is_first_start_zero",
1451 "test_repl_nc_is_first_start_zero_nc_change",
1455 # Only some tests behave any differently with the zeroed
1456 # reserved_usn. The getncchanges tests are quite slow, so it
1457 # is worth skipping unnecessary tests.
1459 # If you think a test is worth running here, add it to the
1461 testname
= self
.id().rsplit(".", 1)[1]
1462 if testname
in self
.SKIPPED_TESTS
:
1463 self
.skipTest("Probably not affected by reserved_usn = 0")
1469 """Helper class to track a connection to another DC"""
1471 def __init__(self
, drs_base
, ldb_dc
, dnsname_dc
):
1472 self
.ldb_dc
= ldb_dc
1473 (self
.drs
, self
.drs_handle
) = drs_base
._ds
_bind
(dnsname_dc
)
1474 (self
.default_hwm
, utdv
) = drs_base
._get
_highest
_hwm
_utdv
(ldb_dc
)
1475 self
.default_utdv
= utdv
1476 self
.dnsname_dc
= dnsname_dc