2 # -*- coding: utf-8 -*-
4 # Tests replication scenarios that involve conflicting linked attribute
5 # information between the 2 DCs.
7 # Copyright (C) Catalyst.Net Ltd. 2017
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # export DC1=dc1_dns_name
26 # export DC2=dc2_dns_name
27 # export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun
28 # PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN link_conflicts -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD"
34 from ldb import SCOPE_BASE
38 from drs_base import AbstractLink
39 from samba.dcerpc import drsuapi, misc
41 # specifies the order to sync DCs in
45 class DrsReplicaLinkConflictTestCase(drs_base.DrsBaseTestCase):
47 super(DrsReplicaLinkConflictTestCase, self).setUp()
49 # add some randomness to the test OU. (Deletion of the last test's
50 # objects can be slow to replicate out. So the OU created by a previous
51 # testenv may still exist at this point).
52 rand = random.randint(1, 10000000)
53 self.base_dn = self.ldb_dc1.get_default_basedn()
54 self.ou = "OU=test_link_conflict%d,%s" %(rand, self.base_dn)
57 "objectclass": "organizationalUnit"})
59 (self.drs, self.drs_handle) = self._ds_bind(self.dnsname_dc1)
60 (self.drs2, self.drs2_handle) = self._ds_bind(self.dnsname_dc2)
62 # disable replication for the tests so we can control at what point
63 # the DCs try to replicate
64 self._disable_inbound_repl(self.dnsname_dc1)
65 self._disable_inbound_repl(self.dnsname_dc2)
68 # re-enable replication
69 self._enable_inbound_repl(self.dnsname_dc1)
70 self._enable_inbound_repl(self.dnsname_dc2)
71 self.ldb_dc1.delete(self.ou, ["tree_delete:1"])
72 super(DrsReplicaLinkConflictTestCase, self).tearDown()
74 def get_guid(self, samdb, dn):
75 """Returns an object's GUID (in string format)"""
76 res = samdb.search(base=dn, attrs=["objectGUID"], scope=ldb.SCOPE_BASE)
77 return self._GUID_string(res[0]['objectGUID'][0])
79 def add_object(self, samdb, dn, objectclass="organizationalunit"):
81 samdb.add({"dn": dn, "objectclass": objectclass})
82 return self.get_guid(samdb, dn)
84 def modify_object(self, samdb, dn, attr, value):
85 """Modifies an attribute for an object"""
87 m.dn = ldb.Dn(samdb, dn)
88 m[attr] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, attr)
91 def add_link_attr(self, samdb, source_dn, attr, target_dn):
92 """Adds a linked attribute between 2 objects"""
93 # add the specified attribute to the source object
94 self.modify_object(samdb, source_dn, attr, target_dn)
96 def del_link_attr(self, samdb, src, attr, target):
98 m.dn = ldb.Dn(samdb, src)
99 m[attr] = ldb.MessageElement(target, ldb.FLAG_MOD_DELETE, attr)
102 def sync_DCs(self, sync_order=DC1_TO_DC2):
103 """Manually syncs the 2 DCs to ensure they're in sync"""
104 if sync_order == DC1_TO_DC2:
105 # sync DC1-->DC2, then DC2-->DC1
106 self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1)
107 self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2)
109 # sync DC2-->DC1, then DC1-->DC2
110 self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2)
111 self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1)
113 def ensure_unique_timestamp(self):
114 """Waits a second to ensure a unique timestamp between 2 objects"""
117 def unique_dn(self, obj_name):
118 """Returns a unique object DN"""
119 # Because we run each test case twice, we need to create a unique DN so
120 # that the 2nd run doesn't hit objects that already exist. Add some
121 # randomness to the object DN to make it unique
122 rand = random.randint(1, 10000000)
123 return "%s-%d,%s" %(obj_name, rand, self.ou)
125 def assert_attrs_match(self, res1, res2, attr, expected_count):
127 Asserts that the search results contain the expected number of
128 attributes and the results match on both DCs
130 actual_len = len(res1[0][attr])
131 self.assertTrue(actual_len == expected_count,
132 "Expected %u %s attributes, but got %u" %(expected_count,
134 actual_len = len(res2[0][attr])
135 self.assertTrue(actual_len == expected_count,
136 "Expected %u %s attributes, but got %u" %(expected_count,
139 # check DCs both agree on the same linked attributes
140 for val in res1[0][attr]:
141 self.assertTrue(val in res2[0][attr],
142 "%s '%s' not found on DC2" %(attr, val))
144 def _check_replicated_links(self, src_obj_dn, expected_links):
145 """Checks that replication sends back the expected linked attributes"""
147 hwm = drsuapi.DsReplicaHighWaterMark()
148 hwm.tmp_highest_usn = 0
152 self._check_replication([src_obj_dn],
153 drsuapi.DRSUAPI_DRS_WRIT_REP,
155 drs_error=drsuapi.DRSUAPI_EXOP_ERR_SUCCESS,
156 nc_dn_str=src_obj_dn,
157 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
158 expected_links=expected_links,
162 self.set_test_ldb_dc(self.ldb_dc2)
164 self._check_replication([src_obj_dn],
165 drsuapi.DRSUAPI_DRS_WRIT_REP,
167 drs_error=drsuapi.DRSUAPI_EXOP_ERR_SUCCESS,
168 nc_dn_str=src_obj_dn,
169 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
170 expected_links=expected_links,
172 drs=self.drs2, drs_handle=self.drs2_handle)
173 self.set_test_ldb_dc(self.ldb_dc1)
175 def _test_conflict_single_valued_link(self, sync_order):
177 Tests a simple single-value link conflict, i.e. each DC adds a link to
178 the same source object but linking to different targets.
180 src_ou = self.unique_dn("OU=src")
181 src_guid = self.add_object(self.ldb_dc1, src_ou)
184 # create a unique target on each DC
185 target1_ou = self.unique_dn("OU=target1")
186 target2_ou = self.unique_dn("OU=target2")
188 target1_guid = self.add_object(self.ldb_dc1, target1_ou)
189 target2_guid = self.add_object(self.ldb_dc2, target2_ou)
191 # link the test OU to the respective targets created
192 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
193 self.ensure_unique_timestamp()
194 self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou)
197 self.sync_DCs(sync_order=sync_order)
199 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
200 scope=SCOPE_BASE, attrs=["managedBy"])
201 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
202 scope=SCOPE_BASE, attrs=["managedBy"])
204 # check the object has only have one occurence of the single-valued
205 # attribute and it matches on both DCs
206 self.assert_attrs_match(res1, res2, "managedBy", 1)
208 self.assertTrue(res1[0]["managedBy"][0] == target2_ou,
209 "Expected most recent update to win conflict")
211 # we can't query the deleted links over LDAP, but we can check DRS
212 # to make sure the DC kept a copy of the conflicting link
213 link1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, 0,
214 misc.GUID(src_guid), misc.GUID(target1_guid))
215 link2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
216 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
217 misc.GUID(src_guid), misc.GUID(target2_guid))
218 self._check_replicated_links(src_ou, [link1, link2])
221 def test_conflict_single_valued_link(self):
222 # repeat the test twice, to give each DC a chance to resolve the conflict
223 self._test_conflict_single_valued_link(sync_order=DC1_TO_DC2)
224 self._test_conflict_single_valued_link(sync_order=DC2_TO_DC1)
226 def _test_duplicate_single_valued_link(self, sync_order):
228 Adds the same single-valued link on 2 DCs and checks we don't end up
229 with 2 copies of the link.
231 # create unique objects for the link
232 target_ou = self.unique_dn("OU=target")
233 target_guid = self.add_object(self.ldb_dc1, target_ou)
234 src_ou = self.unique_dn("OU=src")
235 src_guid = self.add_object(self.ldb_dc1, src_ou)
238 # link the same test OU to the same target on both DCs
239 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target_ou)
240 self.ensure_unique_timestamp()
241 self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target_ou)
244 self.sync_DCs(sync_order=sync_order)
246 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
247 scope=SCOPE_BASE, attrs=["managedBy"])
248 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
249 scope=SCOPE_BASE, attrs=["managedBy"])
251 # check the object has only have one occurence of the single-valued
252 # attribute and it matches on both DCs
253 self.assert_attrs_match(res1, res2, "managedBy", 1)
255 def test_duplicate_single_valued_link(self):
256 # repeat the test twice, to give each DC a chance to resolve the conflict
257 self._test_duplicate_single_valued_link(sync_order=DC1_TO_DC2)
258 self._test_duplicate_single_valued_link(sync_order=DC2_TO_DC1)
260 def _test_conflict_multi_valued_link(self, sync_order):
262 Tests a simple multi-valued link conflict. This adds 2 objects with the
263 same username on 2 different DCs and checks their group membership is
264 preserved after the conflict is resolved.
267 # create a common link source
268 src_dn = self.unique_dn("CN=src")
269 src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group")
272 # create the same user (link target) on each DC.
273 # Note that the GUIDs will differ between the DCs
274 target_dn = self.unique_dn("CN=target")
275 target1_guid = self.add_object(self.ldb_dc1, target_dn, objectclass="user")
276 self.ensure_unique_timestamp()
277 target2_guid = self.add_object(self.ldb_dc2, target_dn, objectclass="user")
279 # link the src group to the respective target created
280 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
281 self.ensure_unique_timestamp()
282 self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
284 # sync the 2 DCs. We expect the more recent target2 object to win
285 self.sync_DCs(sync_order=sync_order)
287 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
288 scope=SCOPE_BASE, attrs=["member"])
289 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
290 scope=SCOPE_BASE, attrs=["member"])
291 target1_conflict = False
293 # we expect exactly 2 members in our test group (both DCs should agree)
294 self.assert_attrs_match(res1, res2, "member", 2)
296 for val in res1[0]["member"]:
297 # check the expected conflicting object was renamed
298 self.assertFalse("CNF:%s" % target2_guid in val)
299 if "CNF:%s" % target1_guid in val:
300 target1_conflict = True
302 self.assertTrue(target1_conflict,
303 "Expected link to conflicting target object not found")
305 def test_conflict_multi_valued_link(self):
306 # repeat the test twice, to give each DC a chance to resolve the conflict
307 self._test_conflict_multi_valued_link(sync_order=DC1_TO_DC2)
308 self._test_conflict_multi_valued_link(sync_order=DC2_TO_DC1)
310 def _test_duplicate_multi_valued_link(self, sync_order):
312 Adds the same multivalued link on 2 DCs and checks we don't end up
313 with 2 copies of the link.
316 # create the link source/target objects
317 src_dn = self.unique_dn("CN=src")
318 src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group")
319 target_dn = self.unique_dn("CN=target")
320 target_guid = self.add_object(self.ldb_dc1, target_dn, objectclass="user")
323 # link the src group to the same target user separately on each DC
324 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
325 self.ensure_unique_timestamp()
326 self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
328 self.sync_DCs(sync_order=sync_order)
330 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
331 scope=SCOPE_BASE, attrs=["member"])
332 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
333 scope=SCOPE_BASE, attrs=["member"])
335 # we expect to still have only 1 member in our test group
336 self.assert_attrs_match(res1, res2, "member", 1)
338 def test_duplicate_multi_valued_link(self):
339 # repeat the test twice, to give each DC a chance to resolve the conflict
340 self._test_duplicate_multi_valued_link(sync_order=DC1_TO_DC2)
341 self._test_duplicate_multi_valued_link(sync_order=DC2_TO_DC1)
343 def _test_conflict_backlinks(self, sync_order):
345 Tests that resolving a source object conflict fixes up any backlinks,
346 e.g. the same user is added to a conflicting group.
349 # create a common link target
350 target_dn = self.unique_dn("CN=target")
351 target_guid = self.add_object(self.ldb_dc1, target_dn, objectclass="user")
354 # create the same group (link source) on each DC.
355 # Note that the GUIDs will differ between the DCs
356 src_dn = self.unique_dn("CN=src")
357 src1_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group")
358 self.ensure_unique_timestamp()
359 src2_guid = self.add_object(self.ldb_dc2, src_dn, objectclass="group")
361 # link the src group to the respective target created
362 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
363 self.ensure_unique_timestamp()
364 self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
366 # sync the 2 DCs. We expect the more recent src2 object to win
367 self.sync_DCs(sync_order=sync_order)
369 res1 = self.ldb_dc1.search(base="<GUID=%s>" % target_guid,
370 scope=SCOPE_BASE, attrs=["memberOf"])
371 res2 = self.ldb_dc2.search(base="<GUID=%s>" % target_guid,
372 scope=SCOPE_BASE, attrs=["memberOf"])
373 src1_backlink = False
375 # our test user should still be a member of 2 groups (check both DCs agree)
376 self.assert_attrs_match(res1, res2, "memberOf", 2)
378 for val in res1[0]["memberOf"]:
379 # check the conflicting object was renamed
380 self.assertFalse("CNF:%s" % src2_guid in val)
381 if "CNF:%s" % src1_guid in val:
384 self.assertTrue(src1_backlink,
385 "Expected backlink to conflicting source object not found")
387 def test_conflict_backlinks(self):
388 # repeat the test twice, to give each DC a chance to resolve the conflict
389 self._test_conflict_backlinks(sync_order=DC1_TO_DC2)
390 self._test_conflict_backlinks(sync_order=DC2_TO_DC1)
392 def _test_link_deletion_conflict(self, sync_order):
394 Checks that a deleted link conflicting with an active link is
398 # Add the link objects
399 target_dn = self.unique_dn("CN=target")
400 target_guid = self.add_object(self.ldb_dc1, target_dn, objectclass="user")
401 src_dn = self.unique_dn("CN=src")
402 src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group")
405 # add the same link on both DCs, and resolve any conflict
406 self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
407 self.ensure_unique_timestamp()
408 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
409 self.sync_DCs(sync_order=sync_order)
411 # delete and re-add the link on one DC
412 self.del_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
413 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
415 # just delete it on the other DC
416 self.ensure_unique_timestamp()
417 self.del_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
418 # sanity-check the link is gone on this DC
419 res1 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
420 scope=SCOPE_BASE, attrs=["member"])
421 self.assertFalse("member" in res1[0], "Couldn't delete member attr")
423 # sync the 2 DCs. We expect the more older DC1 attribute to win
424 # because it has a higher version number (even though it's older)
425 self.sync_DCs(sync_order=sync_order)
427 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
428 scope=SCOPE_BASE, attrs=["member"])
429 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
430 scope=SCOPE_BASE, attrs=["member"])
432 # our test user should still be a member of the group (check both DCs agree)
433 self.assertTrue("member" in res1[0], "Expected member attribute missing")
434 self.assert_attrs_match(res1, res2, "member", 1)
436 def test_link_deletion_conflict(self):
437 # repeat the test twice, to give each DC a chance to resolve the conflict
438 self._test_link_deletion_conflict(sync_order=DC1_TO_DC2)
439 self._test_link_deletion_conflict(sync_order=DC2_TO_DC1)
441 def _test_obj_deletion_conflict(self, sync_order, del_target):
443 Checks that a receiving a new link for a deleted object gets
447 target_dn = self.unique_dn("CN=target")
448 target_guid = self.add_object(self.ldb_dc1, target_dn, objectclass="user")
449 src_dn = self.unique_dn("CN=src")
450 src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group")
454 # delete the object on one DC
456 search_guid = src_guid
457 self.ldb_dc2.delete(target_dn)
459 search_guid = target_guid
460 self.ldb_dc2.delete(src_dn)
462 # add a link on the other DC
463 self.ensure_unique_timestamp()
464 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
466 self.sync_DCs(sync_order=sync_order)
468 # the object deletion should trump the link addition.
469 # Check the link no longer exists on the remaining object
470 res1 = self.ldb_dc1.search(base="<GUID=%s>" % search_guid,
471 scope=SCOPE_BASE, attrs=["member", "memberOf"])
472 res2 = self.ldb_dc2.search(base="<GUID=%s>" % search_guid,
473 scope=SCOPE_BASE, attrs=["member", "memberOf"])
475 self.assertFalse("member" in res1[0], "member attr shouldn't exist")
476 self.assertFalse("member" in res2[0], "member attr shouldn't exist")
477 self.assertFalse("memberOf" in res1[0], "member attr shouldn't exist")
478 self.assertFalse("memberOf" in res2[0], "member attr shouldn't exist")
480 def test_obj_deletion_conflict(self):
481 # repeat the test twice, to give each DC a chance to resolve the conflict
482 self._test_obj_deletion_conflict(sync_order=DC1_TO_DC2, del_target=True)
483 self._test_obj_deletion_conflict(sync_order=DC2_TO_DC1, del_target=True)
485 # and also try deleting the source object instead of the link target
486 self._test_obj_deletion_conflict(sync_order=DC1_TO_DC2, del_target=False)
487 self._test_obj_deletion_conflict(sync_order=DC2_TO_DC1, del_target=False)
489 def _test_full_sync_link_conflict(self, sync_order):
491 Checks that doing a full sync doesn't affect how conflicts get resolved
494 # create the objects for the linked attribute
495 src_dn = self.unique_dn("CN=src")
496 src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group")
497 target_dn = self.unique_dn("CN=target")
498 target1_guid = self.add_object(self.ldb_dc1, target_dn, objectclass="user")
501 # add the same link on both DCs
502 self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
503 self.ensure_unique_timestamp()
504 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
506 # Do a couple of full syncs which should resolve the conflict
507 # (but only for one DC)
508 if sync_order == DC1_TO_DC2:
509 self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, full_sync=True)
510 self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, full_sync=True)
512 self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, full_sync=True)
513 self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, full_sync=True)
515 # delete and re-add the link on one DC
516 self.del_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
517 self.ensure_unique_timestamp()
518 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
520 # just delete the link on the 2nd DC
521 self.ensure_unique_timestamp()
522 self.del_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
524 # sync the 2 DCs. We expect DC1 to win based on version number
525 self.sync_DCs(sync_order=sync_order)
527 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
528 scope=SCOPE_BASE, attrs=["member"])
529 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
530 scope=SCOPE_BASE, attrs=["member"])
532 # check the membership still exits (and both DCs agree)
533 self.assertTrue("member" in res1[0], "Expected member attribute missing")
534 self.assert_attrs_match(res1, res2, "member", 1)
536 def test_full_sync_link_conflict(self):
537 # repeat the test twice, to give each DC a chance to resolve the conflict
538 self._test_full_sync_link_conflict(sync_order=DC1_TO_DC2)
539 self._test_full_sync_link_conflict(sync_order=DC2_TO_DC1)
541 def _test_conflict_single_valued_link_deleted_winner(self, sync_order):
543 Tests a single-value link conflict where the more-up-to-date link value
546 src_ou = self.unique_dn("OU=src")
547 src_guid = self.add_object(self.ldb_dc1, src_ou)
550 # create a unique target on each DC
551 target1_ou = self.unique_dn("OU=target1")
552 target2_ou = self.unique_dn("OU=target2")
554 target1_guid = self.add_object(self.ldb_dc1, target1_ou)
555 target2_guid = self.add_object(self.ldb_dc2, target2_ou)
557 # add the links for the respective targets, and delete one of the links
558 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
559 self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou)
560 self.ensure_unique_timestamp()
561 self.del_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
564 self.sync_DCs(sync_order=sync_order)
566 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
567 scope=SCOPE_BASE, attrs=["managedBy"])
568 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
569 scope=SCOPE_BASE, attrs=["managedBy"])
571 # Although the more up-to-date link value is deleted, this shouldn't
572 # trump DC1's active link
573 self.assert_attrs_match(res1, res2, "managedBy", 1)
575 self.assertTrue(res1[0]["managedBy"][0] == target2_ou,
576 "Expected active link win conflict")
578 # we can't query the deleted links over LDAP, but we can check that
579 # the deleted links exist using DRS
580 link1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, 0,
581 misc.GUID(src_guid), misc.GUID(target1_guid))
582 link2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
583 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
584 misc.GUID(src_guid), misc.GUID(target2_guid))
585 self._check_replicated_links(src_ou, [link1, link2])
587 def test_conflict_single_valued_link_deleted_winner(self):
588 # repeat the test twice, to give each DC a chance to resolve the conflict
589 self._test_conflict_single_valued_link_deleted_winner(sync_order=DC1_TO_DC2)
590 self._test_conflict_single_valued_link_deleted_winner(sync_order=DC2_TO_DC1)
592 def _test_conflict_single_valued_link_deleted_loser(self, sync_order):
594 Tests a single-valued link conflict, where the losing link value is deleted.
596 src_ou = self.unique_dn("OU=src")
597 src_guid = self.add_object(self.ldb_dc1, src_ou)
600 # create a unique target on each DC
601 target1_ou = self.unique_dn("OU=target1")
602 target2_ou = self.unique_dn("OU=target2")
604 target1_guid = self.add_object(self.ldb_dc1, target1_ou)
605 target2_guid = self.add_object(self.ldb_dc2, target2_ou)
607 # add the links - we want the link to end up deleted on DC2, but active on
608 # DC1. DC1 has the better version and DC2 has the better timestamp - the
609 # better version should win
610 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
611 self.del_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
612 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
613 self.ensure_unique_timestamp()
614 self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou)
615 self.del_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou)
617 self.sync_DCs(sync_order=sync_order)
619 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
620 scope=SCOPE_BASE, attrs=["managedBy"])
621 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
622 scope=SCOPE_BASE, attrs=["managedBy"])
624 # check the object has only have one occurence of the single-valued
625 # attribute and it matches on both DCs
626 self.assert_attrs_match(res1, res2, "managedBy", 1)
628 self.assertTrue(res1[0]["managedBy"][0] == target1_ou,
629 "Expected most recent update to win conflict")
631 # we can't query the deleted links over LDAP, but we can check DRS
632 # to make sure the DC kept a copy of the conflicting link
633 link1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
634 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
635 misc.GUID(src_guid), misc.GUID(target1_guid))
636 link2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, 0,
637 misc.GUID(src_guid), misc.GUID(target2_guid))
638 self._check_replicated_links(src_ou, [link1, link2])
640 def test_conflict_single_valued_link_deleted_loser(self):
641 # repeat the test twice, to give each DC a chance to resolve the conflict
642 self._test_conflict_single_valued_link_deleted_loser(sync_order=DC1_TO_DC2)
643 self._test_conflict_single_valued_link_deleted_loser(sync_order=DC2_TO_DC1)
645 def _test_conflict_existing_single_valued_link(self, sync_order):
647 Tests a single-valued link conflict, where the conflicting link value
648 already exists (as inactive) on both DCs.
650 # create the link objects
651 src_ou = self.unique_dn("OU=src")
652 src_guid = self.add_object(self.ldb_dc1, src_ou)
654 target1_ou = self.unique_dn("OU=target1")
655 target2_ou = self.unique_dn("OU=target2")
656 target1_guid = self.add_object(self.ldb_dc1, target1_ou)
657 target2_guid = self.add_object(self.ldb_dc1, target2_ou)
659 # add the links, but then delete them
660 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
661 self.del_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
662 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target2_ou)
663 self.del_link_attr(self.ldb_dc1, src_ou, "managedBy", target2_ou)
666 # re-add the links independently on each DC
667 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
668 self.ensure_unique_timestamp()
669 self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou)
671 # try to sync the 2 DCs (this currently fails)
673 self.sync_DCs(sync_order=sync_order)
675 self.fail("Replication could not resolve link conflict: %s" % e)
677 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
678 scope=SCOPE_BASE, attrs=["managedBy"])
679 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
680 scope=SCOPE_BASE, attrs=["managedBy"])
682 # check the object has only have one occurence of the single-valued
683 # attribute and it matches on both DCs
684 self.assert_attrs_match(res1, res2, "managedBy", 1)
686 # here we expect DC2 to win because it has the more recent link
687 self.assertTrue(res1[0]["managedBy"][0] == target2_ou,
688 "Expected most recent update to win conflict")
690 # we can't query the deleted links over LDAP, but we can check DRS
691 # to make sure the DC kept a copy of the conflicting link
692 link1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, 0,
693 misc.GUID(src_guid), misc.GUID(target1_guid))
694 link2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
695 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
696 misc.GUID(src_guid), misc.GUID(target2_guid))
697 self._check_replicated_links(src_ou, [link1, link2])
699 def test_conflict_existing_single_valued_link(self):
700 # repeat the test twice, to give each DC a chance to resolve the conflict
701 self._test_conflict_existing_single_valued_link(sync_order=DC1_TO_DC2)
702 self._test_conflict_existing_single_valued_link(sync_order=DC2_TO_DC1)