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 self.ou = samba.tests.create_test_ou(self.ldb_dc1, "test_link_conflict")
50 self.base_dn = self.ldb_dc1.get_default_basedn()
52 (self.drs, self.drs_handle) = self._ds_bind(self.dnsname_dc1)
53 (self.drs2, self.drs2_handle) = self._ds_bind(self.dnsname_dc2)
55 # disable replication for the tests so we can control at what point
56 # the DCs try to replicate
57 self._disable_inbound_repl(self.dnsname_dc1)
58 self._disable_inbound_repl(self.dnsname_dc2)
61 # re-enable replication
62 self._enable_inbound_repl(self.dnsname_dc1)
63 self._enable_inbound_repl(self.dnsname_dc2)
64 self.ldb_dc1.delete(self.ou, ["tree_delete:1"])
65 super(DrsReplicaLinkConflictTestCase, self).tearDown()
67 def get_guid(self, samdb, dn):
68 """Returns an object's GUID (in string format)"""
69 res = samdb.search(base=dn, attrs=["objectGUID"], scope=ldb.SCOPE_BASE)
70 return self._GUID_string(res[0]['objectGUID'][0])
72 def add_object(self, samdb, dn, objectclass="organizationalunit"):
74 samdb.add({"dn": dn, "objectclass": objectclass})
75 return self.get_guid(samdb, dn)
77 def modify_object(self, samdb, dn, attr, value):
78 """Modifies an attribute for an object"""
80 m.dn = ldb.Dn(samdb, dn)
81 m[attr] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, attr)
84 def add_link_attr(self, samdb, source_dn, attr, target_dn):
85 """Adds a linked attribute between 2 objects"""
86 # add the specified attribute to the source object
87 self.modify_object(samdb, source_dn, attr, target_dn)
89 def del_link_attr(self, samdb, src, attr, target):
91 m.dn = ldb.Dn(samdb, src)
92 m[attr] = ldb.MessageElement(target, ldb.FLAG_MOD_DELETE, attr)
95 def sync_DCs(self, sync_order=DC1_TO_DC2):
96 """Manually syncs the 2 DCs to ensure they're in sync"""
97 if sync_order == DC1_TO_DC2:
98 # sync DC1-->DC2, then DC2-->DC1
99 self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1)
100 self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2)
102 # sync DC2-->DC1, then DC1-->DC2
103 self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2)
104 self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1)
106 def ensure_unique_timestamp(self):
107 """Waits a second to ensure a unique timestamp between 2 objects"""
110 def unique_dn(self, obj_name):
111 """Returns a unique object DN"""
112 # Because we run each test case twice, we need to create a unique DN so
113 # that the 2nd run doesn't hit objects that already exist. Add some
114 # randomness to the object DN to make it unique
115 rand = random.randint(1, 10000000)
116 return "%s-%d,%s" %(obj_name, rand, self.ou)
118 def assert_attrs_match(self, res1, res2, attr, expected_count):
120 Asserts that the search results contain the expected number of
121 attributes and the results match on both DCs
123 actual_len = len(res1[0][attr])
124 self.assertTrue(actual_len == expected_count,
125 "Expected %u %s attributes, but got %u" %(expected_count,
127 actual_len = len(res2[0][attr])
128 self.assertTrue(actual_len == expected_count,
129 "Expected %u %s attributes, but got %u" %(expected_count,
132 # check DCs both agree on the same linked attributes
133 for val in res1[0][attr]:
134 self.assertTrue(val in res2[0][attr],
135 "%s '%s' not found on DC2" %(attr, val))
137 def zero_highwatermark(self):
138 """Returns a zeroed highwatermark so that all DRS data gets returned"""
139 hwm = drsuapi.DsReplicaHighWaterMark()
140 hwm.tmp_highest_usn = 0
145 def _check_replicated_links(self, src_obj_dn, expected_links):
146 """Checks that replication sends back the expected linked attributes"""
147 self._check_replication([src_obj_dn],
148 drsuapi.DRSUAPI_DRS_WRIT_REP,
150 drs_error=drsuapi.DRSUAPI_EXOP_ERR_SUCCESS,
151 nc_dn_str=src_obj_dn,
152 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
153 expected_links=expected_links,
154 highwatermark=self.zero_highwatermark())
157 self.set_test_ldb_dc(self.ldb_dc2)
159 self._check_replication([src_obj_dn],
160 drsuapi.DRSUAPI_DRS_WRIT_REP,
162 drs_error=drsuapi.DRSUAPI_EXOP_ERR_SUCCESS,
163 nc_dn_str=src_obj_dn,
164 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
165 expected_links=expected_links,
166 highwatermark=self.zero_highwatermark(),
167 drs=self.drs2, drs_handle=self.drs2_handle)
168 self.set_test_ldb_dc(self.ldb_dc1)
170 def _test_conflict_single_valued_link(self, sync_order):
172 Tests a simple single-value link conflict, i.e. each DC adds a link to
173 the same source object but linking to different targets.
175 src_ou = self.unique_dn("OU=src")
176 src_guid = self.add_object(self.ldb_dc1, src_ou)
179 # create a unique target on each DC
180 target1_ou = self.unique_dn("OU=target1")
181 target2_ou = self.unique_dn("OU=target2")
183 target1_guid = self.add_object(self.ldb_dc1, target1_ou)
184 target2_guid = self.add_object(self.ldb_dc2, target2_ou)
186 # link the test OU to the respective targets created
187 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
188 self.ensure_unique_timestamp()
189 self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou)
192 self.sync_DCs(sync_order=sync_order)
194 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
195 scope=SCOPE_BASE, attrs=["managedBy"])
196 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
197 scope=SCOPE_BASE, attrs=["managedBy"])
199 # check the object has only have one occurence of the single-valued
200 # attribute and it matches on both DCs
201 self.assert_attrs_match(res1, res2, "managedBy", 1)
203 self.assertTrue(res1[0]["managedBy"][0] == target2_ou,
204 "Expected most recent update to win conflict")
206 # we can't query the deleted links over LDAP, but we can check DRS
207 # to make sure the DC kept a copy of the conflicting link
208 link1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, 0,
209 misc.GUID(src_guid), misc.GUID(target1_guid))
210 link2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
211 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
212 misc.GUID(src_guid), misc.GUID(target2_guid))
213 self._check_replicated_links(src_ou, [link1, link2])
216 def test_conflict_single_valued_link(self):
217 # repeat the test twice, to give each DC a chance to resolve the conflict
218 self._test_conflict_single_valued_link(sync_order=DC1_TO_DC2)
219 self._test_conflict_single_valued_link(sync_order=DC2_TO_DC1)
221 def _test_duplicate_single_valued_link(self, sync_order):
223 Adds the same single-valued link on 2 DCs and checks we don't end up
224 with 2 copies of the link.
226 # create unique objects for the link
227 target_ou = self.unique_dn("OU=target")
228 self.add_object(self.ldb_dc1, target_ou)
229 src_ou = self.unique_dn("OU=src")
230 src_guid = self.add_object(self.ldb_dc1, src_ou)
233 # link the same test OU to the same target on both DCs
234 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target_ou)
235 self.ensure_unique_timestamp()
236 self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target_ou)
239 self.sync_DCs(sync_order=sync_order)
241 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
242 scope=SCOPE_BASE, attrs=["managedBy"])
243 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
244 scope=SCOPE_BASE, attrs=["managedBy"])
246 # check the object has only have one occurence of the single-valued
247 # attribute and it matches on both DCs
248 self.assert_attrs_match(res1, res2, "managedBy", 1)
250 def test_duplicate_single_valued_link(self):
251 # repeat the test twice, to give each DC a chance to resolve the conflict
252 self._test_duplicate_single_valued_link(sync_order=DC1_TO_DC2)
253 self._test_duplicate_single_valued_link(sync_order=DC2_TO_DC1)
255 def _test_conflict_multi_valued_link(self, sync_order):
257 Tests a simple multi-valued link conflict. This adds 2 objects with the
258 same username on 2 different DCs and checks their group membership is
259 preserved after the conflict is resolved.
262 # create a common link source
263 src_dn = self.unique_dn("CN=src")
264 src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group")
267 # create the same user (link target) on each DC.
268 # Note that the GUIDs will differ between the DCs
269 target_dn = self.unique_dn("CN=target")
270 target1_guid = self.add_object(self.ldb_dc1, target_dn, objectclass="user")
271 self.ensure_unique_timestamp()
272 target2_guid = self.add_object(self.ldb_dc2, target_dn, objectclass="user")
274 # link the src group to the respective target created
275 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
276 self.ensure_unique_timestamp()
277 self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
279 # sync the 2 DCs. We expect the more recent target2 object to win
280 self.sync_DCs(sync_order=sync_order)
282 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
283 scope=SCOPE_BASE, attrs=["member"])
284 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
285 scope=SCOPE_BASE, attrs=["member"])
286 target1_conflict = False
288 # we expect exactly 2 members in our test group (both DCs should agree)
289 self.assert_attrs_match(res1, res2, "member", 2)
291 for val in res1[0]["member"]:
292 # check the expected conflicting object was renamed
293 self.assertFalse("CNF:%s" % target2_guid in val)
294 if "CNF:%s" % target1_guid in val:
295 target1_conflict = True
297 self.assertTrue(target1_conflict,
298 "Expected link to conflicting target object not found")
300 def test_conflict_multi_valued_link(self):
301 # repeat the test twice, to give each DC a chance to resolve the conflict
302 self._test_conflict_multi_valued_link(sync_order=DC1_TO_DC2)
303 self._test_conflict_multi_valued_link(sync_order=DC2_TO_DC1)
305 def _test_duplicate_multi_valued_link(self, sync_order):
307 Adds the same multivalued link on 2 DCs and checks we don't end up
308 with 2 copies of the link.
311 # create the link source/target objects
312 src_dn = self.unique_dn("CN=src")
313 src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group")
314 target_dn = self.unique_dn("CN=target")
315 self.add_object(self.ldb_dc1, target_dn, objectclass="user")
318 # link the src group to the same target user separately on each DC
319 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
320 self.ensure_unique_timestamp()
321 self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
323 self.sync_DCs(sync_order=sync_order)
325 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
326 scope=SCOPE_BASE, attrs=["member"])
327 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
328 scope=SCOPE_BASE, attrs=["member"])
330 # we expect to still have only 1 member in our test group
331 self.assert_attrs_match(res1, res2, "member", 1)
333 def test_duplicate_multi_valued_link(self):
334 # repeat the test twice, to give each DC a chance to resolve the conflict
335 self._test_duplicate_multi_valued_link(sync_order=DC1_TO_DC2)
336 self._test_duplicate_multi_valued_link(sync_order=DC2_TO_DC1)
338 def _test_conflict_backlinks(self, sync_order):
340 Tests that resolving a source object conflict fixes up any backlinks,
341 e.g. the same user is added to a conflicting group.
344 # create a common link target
345 target_dn = self.unique_dn("CN=target")
346 target_guid = self.add_object(self.ldb_dc1, target_dn, objectclass="user")
349 # create the same group (link source) on each DC.
350 # Note that the GUIDs will differ between the DCs
351 src_dn = self.unique_dn("CN=src")
352 src1_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group")
353 self.ensure_unique_timestamp()
354 src2_guid = self.add_object(self.ldb_dc2, src_dn, objectclass="group")
356 # link the src group to the respective target created
357 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
358 self.ensure_unique_timestamp()
359 self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
361 # sync the 2 DCs. We expect the more recent src2 object to win
362 self.sync_DCs(sync_order=sync_order)
364 res1 = self.ldb_dc1.search(base="<GUID=%s>" % target_guid,
365 scope=SCOPE_BASE, attrs=["memberOf"])
366 res2 = self.ldb_dc2.search(base="<GUID=%s>" % target_guid,
367 scope=SCOPE_BASE, attrs=["memberOf"])
368 src1_backlink = False
370 # our test user should still be a member of 2 groups (check both DCs agree)
371 self.assert_attrs_match(res1, res2, "memberOf", 2)
373 for val in res1[0]["memberOf"]:
374 # check the conflicting object was renamed
375 self.assertFalse("CNF:%s" % src2_guid in val)
376 if "CNF:%s" % src1_guid in val:
379 self.assertTrue(src1_backlink,
380 "Expected backlink to conflicting source object not found")
382 def test_conflict_backlinks(self):
383 # repeat the test twice, to give each DC a chance to resolve the conflict
384 self._test_conflict_backlinks(sync_order=DC1_TO_DC2)
385 self._test_conflict_backlinks(sync_order=DC2_TO_DC1)
387 def _test_link_deletion_conflict(self, sync_order):
389 Checks that a deleted link conflicting with an active link is
393 # Add the link objects
394 target_dn = self.unique_dn("CN=target")
395 self.add_object(self.ldb_dc1, target_dn, objectclass="user")
396 src_dn = self.unique_dn("CN=src")
397 src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group")
400 # add the same link on both DCs, and resolve any conflict
401 self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
402 self.ensure_unique_timestamp()
403 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
404 self.sync_DCs(sync_order=sync_order)
406 # delete and re-add the link on one DC
407 self.del_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
408 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
410 # just delete it on the other DC
411 self.ensure_unique_timestamp()
412 self.del_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
413 # sanity-check the link is gone on this DC
414 res1 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
415 scope=SCOPE_BASE, attrs=["member"])
416 self.assertFalse("member" in res1[0], "Couldn't delete member attr")
418 # sync the 2 DCs. We expect the more older DC1 attribute to win
419 # because it has a higher version number (even though it's older)
420 self.sync_DCs(sync_order=sync_order)
422 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
423 scope=SCOPE_BASE, attrs=["member"])
424 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
425 scope=SCOPE_BASE, attrs=["member"])
427 # our test user should still be a member of the group (check both DCs agree)
428 self.assertTrue("member" in res1[0], "Expected member attribute missing")
429 self.assert_attrs_match(res1, res2, "member", 1)
431 def test_link_deletion_conflict(self):
432 # repeat the test twice, to give each DC a chance to resolve the conflict
433 self._test_link_deletion_conflict(sync_order=DC1_TO_DC2)
434 self._test_link_deletion_conflict(sync_order=DC2_TO_DC1)
436 def _test_obj_deletion_conflict(self, sync_order, del_target):
438 Checks that a receiving a new link for a deleted object gets
442 target_dn = self.unique_dn("CN=target")
443 target_guid = self.add_object(self.ldb_dc1, target_dn, objectclass="user")
444 src_dn = self.unique_dn("CN=src")
445 src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group")
449 # delete the object on one DC
451 search_guid = src_guid
452 self.ldb_dc2.delete(target_dn)
454 search_guid = target_guid
455 self.ldb_dc2.delete(src_dn)
457 # add a link on the other DC
458 self.ensure_unique_timestamp()
459 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
461 self.sync_DCs(sync_order=sync_order)
463 # the object deletion should trump the link addition.
464 # Check the link no longer exists on the remaining object
465 res1 = self.ldb_dc1.search(base="<GUID=%s>" % search_guid,
466 scope=SCOPE_BASE, attrs=["member", "memberOf"])
467 res2 = self.ldb_dc2.search(base="<GUID=%s>" % search_guid,
468 scope=SCOPE_BASE, attrs=["member", "memberOf"])
470 self.assertFalse("member" in res1[0], "member attr shouldn't exist")
471 self.assertFalse("member" in res2[0], "member attr shouldn't exist")
472 self.assertFalse("memberOf" in res1[0], "member attr shouldn't exist")
473 self.assertFalse("memberOf" in res2[0], "member attr shouldn't exist")
475 def test_obj_deletion_conflict(self):
476 # repeat the test twice, to give each DC a chance to resolve the conflict
477 self._test_obj_deletion_conflict(sync_order=DC1_TO_DC2, del_target=True)
478 self._test_obj_deletion_conflict(sync_order=DC2_TO_DC1, del_target=True)
480 # and also try deleting the source object instead of the link target
481 self._test_obj_deletion_conflict(sync_order=DC1_TO_DC2, del_target=False)
482 self._test_obj_deletion_conflict(sync_order=DC2_TO_DC1, del_target=False)
484 def _test_full_sync_link_conflict(self, sync_order):
486 Checks that doing a full sync doesn't affect how conflicts get resolved
489 # create the objects for the linked attribute
490 src_dn = self.unique_dn("CN=src")
491 src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group")
492 target_dn = self.unique_dn("CN=target")
493 self.add_object(self.ldb_dc1, target_dn, objectclass="user")
496 # add the same link on both DCs
497 self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
498 self.ensure_unique_timestamp()
499 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
501 # Do a couple of full syncs which should resolve the conflict
502 # (but only for one DC)
503 if sync_order == DC1_TO_DC2:
504 self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, full_sync=True)
505 self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, full_sync=True)
507 self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, full_sync=True)
508 self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, full_sync=True)
510 # delete and re-add the link on one DC
511 self.del_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
512 self.ensure_unique_timestamp()
513 self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn)
515 # just delete the link on the 2nd DC
516 self.ensure_unique_timestamp()
517 self.del_link_attr(self.ldb_dc2, src_dn, "member", target_dn)
519 # sync the 2 DCs. We expect DC1 to win based on version number
520 self.sync_DCs(sync_order=sync_order)
522 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
523 scope=SCOPE_BASE, attrs=["member"])
524 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
525 scope=SCOPE_BASE, attrs=["member"])
527 # check the membership still exits (and both DCs agree)
528 self.assertTrue("member" in res1[0], "Expected member attribute missing")
529 self.assert_attrs_match(res1, res2, "member", 1)
531 def test_full_sync_link_conflict(self):
532 # repeat the test twice, to give each DC a chance to resolve the conflict
533 self._test_full_sync_link_conflict(sync_order=DC1_TO_DC2)
534 self._test_full_sync_link_conflict(sync_order=DC2_TO_DC1)
536 def _test_conflict_single_valued_link_deleted_winner(self, sync_order):
538 Tests a single-value link conflict where the more-up-to-date link value
541 src_ou = self.unique_dn("OU=src")
542 src_guid = self.add_object(self.ldb_dc1, src_ou)
545 # create a unique target on each DC
546 target1_ou = self.unique_dn("OU=target1")
547 target2_ou = self.unique_dn("OU=target2")
549 target1_guid = self.add_object(self.ldb_dc1, target1_ou)
550 target2_guid = self.add_object(self.ldb_dc2, target2_ou)
552 # add the links for the respective targets, and delete one of the links
553 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
554 self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou)
555 self.ensure_unique_timestamp()
556 self.del_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
559 self.sync_DCs(sync_order=sync_order)
561 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
562 scope=SCOPE_BASE, attrs=["managedBy"])
563 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
564 scope=SCOPE_BASE, attrs=["managedBy"])
566 # Although the more up-to-date link value is deleted, this shouldn't
567 # trump DC1's active link
568 self.assert_attrs_match(res1, res2, "managedBy", 1)
570 self.assertTrue(res1[0]["managedBy"][0] == target2_ou,
571 "Expected active link win conflict")
573 # we can't query the deleted links over LDAP, but we can check that
574 # the deleted links exist using DRS
575 link1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, 0,
576 misc.GUID(src_guid), misc.GUID(target1_guid))
577 link2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
578 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
579 misc.GUID(src_guid), misc.GUID(target2_guid))
580 self._check_replicated_links(src_ou, [link1, link2])
582 def test_conflict_single_valued_link_deleted_winner(self):
583 # repeat the test twice, to give each DC a chance to resolve the conflict
584 self._test_conflict_single_valued_link_deleted_winner(sync_order=DC1_TO_DC2)
585 self._test_conflict_single_valued_link_deleted_winner(sync_order=DC2_TO_DC1)
587 def _test_conflict_single_valued_link_deleted_loser(self, sync_order):
589 Tests a single-valued link conflict, where the losing link value is deleted.
591 src_ou = self.unique_dn("OU=src")
592 src_guid = self.add_object(self.ldb_dc1, src_ou)
595 # create a unique target on each DC
596 target1_ou = self.unique_dn("OU=target1")
597 target2_ou = self.unique_dn("OU=target2")
599 target1_guid = self.add_object(self.ldb_dc1, target1_ou)
600 target2_guid = self.add_object(self.ldb_dc2, target2_ou)
602 # add the links - we want the link to end up deleted on DC2, but active on
603 # DC1. DC1 has the better version and DC2 has the better timestamp - the
604 # better version should win
605 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
606 self.del_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
607 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
608 self.ensure_unique_timestamp()
609 self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou)
610 self.del_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou)
612 self.sync_DCs(sync_order=sync_order)
614 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
615 scope=SCOPE_BASE, attrs=["managedBy"])
616 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
617 scope=SCOPE_BASE, attrs=["managedBy"])
619 # check the object has only have one occurence of the single-valued
620 # attribute and it matches on both DCs
621 self.assert_attrs_match(res1, res2, "managedBy", 1)
623 self.assertTrue(res1[0]["managedBy"][0] == target1_ou,
624 "Expected most recent update to win conflict")
626 # we can't query the deleted links over LDAP, but we can check DRS
627 # to make sure the DC kept a copy of the conflicting link
628 link1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
629 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
630 misc.GUID(src_guid), misc.GUID(target1_guid))
631 link2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, 0,
632 misc.GUID(src_guid), misc.GUID(target2_guid))
633 self._check_replicated_links(src_ou, [link1, link2])
635 def test_conflict_single_valued_link_deleted_loser(self):
636 # repeat the test twice, to give each DC a chance to resolve the conflict
637 self._test_conflict_single_valued_link_deleted_loser(sync_order=DC1_TO_DC2)
638 self._test_conflict_single_valued_link_deleted_loser(sync_order=DC2_TO_DC1)
640 def _test_conflict_existing_single_valued_link(self, sync_order):
642 Tests a single-valued link conflict, where the conflicting link value
643 already exists (as inactive) on both DCs.
645 # create the link objects
646 src_ou = self.unique_dn("OU=src")
647 src_guid = self.add_object(self.ldb_dc1, src_ou)
649 target1_ou = self.unique_dn("OU=target1")
650 target2_ou = self.unique_dn("OU=target2")
651 target1_guid = self.add_object(self.ldb_dc1, target1_ou)
652 target2_guid = self.add_object(self.ldb_dc1, target2_ou)
654 # add the links, but then delete them
655 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
656 self.del_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
657 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target2_ou)
658 self.del_link_attr(self.ldb_dc1, src_ou, "managedBy", target2_ou)
661 # re-add the links independently on each DC
662 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
663 self.ensure_unique_timestamp()
664 self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou)
666 # try to sync the 2 DCs
667 self.sync_DCs(sync_order=sync_order)
669 res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid,
670 scope=SCOPE_BASE, attrs=["managedBy"])
671 res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid,
672 scope=SCOPE_BASE, attrs=["managedBy"])
674 # check the object has only have one occurence of the single-valued
675 # attribute and it matches on both DCs
676 self.assert_attrs_match(res1, res2, "managedBy", 1)
678 # here we expect DC2 to win because it has the more recent link
679 self.assertTrue(res1[0]["managedBy"][0] == target2_ou,
680 "Expected most recent update to win conflict")
682 # we can't query the deleted links over LDAP, but we can check DRS
683 # to make sure the DC kept a copy of the conflicting link
684 link1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, 0,
685 misc.GUID(src_guid), misc.GUID(target1_guid))
686 link2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
687 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
688 misc.GUID(src_guid), misc.GUID(target2_guid))
689 self._check_replicated_links(src_ou, [link1, link2])
691 def test_conflict_existing_single_valued_link(self):
692 # repeat the test twice, to give each DC a chance to resolve the conflict
693 self._test_conflict_existing_single_valued_link(sync_order=DC1_TO_DC2)
694 self._test_conflict_existing_single_valued_link(sync_order=DC2_TO_DC1)
696 def test_link_attr_version(self):
698 Checks the link attribute version starts from the correct value
700 # create some objects and add a link
701 src_ou = self.unique_dn("OU=src")
702 self.add_object(self.ldb_dc1, src_ou)
703 target1_ou = self.unique_dn("OU=target1")
704 self.add_object(self.ldb_dc1, target1_ou)
705 self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou)
707 # get the link info via replication
708 ctr6 = self._get_replication(drsuapi.DRSUAPI_DRS_WRIT_REP,
710 drs_error=drsuapi.DRSUAPI_EXOP_ERR_SUCCESS,
711 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
712 highwatermark=self.zero_highwatermark(),
715 self.assertTrue(ctr6.linked_attributes_count == 1,
716 "DRS didn't return a link")
717 link = ctr6.linked_attributes[0]
718 self.assertTrue(link.meta_data.version == 1,
719 "Link version started from %u, not 1" % link.meta_data.version)