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
)
196 # try to sync the 2 DCs (this currently fails)
198 self
.sync_DCs(sync_order
=sync_order
)
200 self
.fail("Replication could not resolve link conflict: %s" % e
)
202 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
203 scope
=SCOPE_BASE
, attrs
=["managedBy"])
204 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
205 scope
=SCOPE_BASE
, attrs
=["managedBy"])
207 # check the object has only have one occurence of the single-valued
208 # attribute and it matches on both DCs
209 self
.assert_attrs_match(res1
, res2
, "managedBy", 1)
211 self
.assertTrue(res1
[0]["managedBy"][0] == target2_ou
,
212 "Expected most recent update to win conflict")
214 # we can't query the deleted links over LDAP, but we can check DRS
215 # to make sure the DC kept a copy of the conflicting link
216 link1
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
, 0,
217 misc
.GUID(src_guid
), misc
.GUID(target1_guid
))
218 link2
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
,
219 drsuapi
.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
,
220 misc
.GUID(src_guid
), misc
.GUID(target2_guid
))
221 self
._check
_replicated
_links
(src_ou
, [link1
, link2
])
224 def test_conflict_single_valued_link(self
):
225 # repeat the test twice, to give each DC a chance to resolve the conflict
226 self
._test
_conflict
_single
_valued
_link
(sync_order
=DC1_TO_DC2
)
227 self
._test
_conflict
_single
_valued
_link
(sync_order
=DC2_TO_DC1
)
229 def _test_duplicate_single_valued_link(self
, sync_order
):
231 Adds the same single-valued link on 2 DCs and checks we don't end up
232 with 2 copies of the link.
234 # create unique objects for the link
235 target_ou
= self
.unique_dn("OU=target")
236 target_guid
= self
.add_object(self
.ldb_dc1
, target_ou
)
237 src_ou
= self
.unique_dn("OU=src")
238 src_guid
= self
.add_object(self
.ldb_dc1
, src_ou
)
241 # link the same test OU to the same target on both DCs
242 self
.add_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target_ou
)
243 self
.ensure_unique_timestamp()
244 self
.add_link_attr(self
.ldb_dc2
, src_ou
, "managedBy", target_ou
)
247 self
.sync_DCs(sync_order
=sync_order
)
249 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
250 scope
=SCOPE_BASE
, attrs
=["managedBy"])
251 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
252 scope
=SCOPE_BASE
, attrs
=["managedBy"])
254 # check the object has only have one occurence of the single-valued
255 # attribute and it matches on both DCs
256 self
.assert_attrs_match(res1
, res2
, "managedBy", 1)
258 def test_duplicate_single_valued_link(self
):
259 # repeat the test twice, to give each DC a chance to resolve the conflict
260 self
._test
_duplicate
_single
_valued
_link
(sync_order
=DC1_TO_DC2
)
261 self
._test
_duplicate
_single
_valued
_link
(sync_order
=DC2_TO_DC1
)
263 def _test_conflict_multi_valued_link(self
, sync_order
):
265 Tests a simple multi-valued link conflict. This adds 2 objects with the
266 same username on 2 different DCs and checks their group membership is
267 preserved after the conflict is resolved.
270 # create a common link source
271 src_dn
= self
.unique_dn("CN=src")
272 src_guid
= self
.add_object(self
.ldb_dc1
, src_dn
, objectclass
="group")
275 # create the same user (link target) on each DC.
276 # Note that the GUIDs will differ between the DCs
277 target_dn
= self
.unique_dn("CN=target")
278 target1_guid
= self
.add_object(self
.ldb_dc1
, target_dn
, objectclass
="user")
279 self
.ensure_unique_timestamp()
280 target2_guid
= self
.add_object(self
.ldb_dc2
, target_dn
, objectclass
="user")
282 # link the src group to the respective target created
283 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
284 self
.ensure_unique_timestamp()
285 self
.add_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
287 # sync the 2 DCs. We expect the more recent target2 object to win
288 self
.sync_DCs(sync_order
=sync_order
)
290 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
291 scope
=SCOPE_BASE
, attrs
=["member"])
292 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
293 scope
=SCOPE_BASE
, attrs
=["member"])
294 target1_conflict
= False
296 # we expect exactly 2 members in our test group (both DCs should agree)
297 self
.assert_attrs_match(res1
, res2
, "member", 2)
299 for val
in res1
[0]["member"]:
300 # check the expected conflicting object was renamed
301 self
.assertFalse("CNF:%s" % target2_guid
in val
)
302 if "CNF:%s" % target1_guid
in val
:
303 target1_conflict
= True
305 self
.assertTrue(target1_conflict
,
306 "Expected link to conflicting target object not found")
308 def test_conflict_multi_valued_link(self
):
309 # repeat the test twice, to give each DC a chance to resolve the conflict
310 self
._test
_conflict
_multi
_valued
_link
(sync_order
=DC1_TO_DC2
)
311 self
._test
_conflict
_multi
_valued
_link
(sync_order
=DC2_TO_DC1
)
313 def _test_duplicate_multi_valued_link(self
, sync_order
):
315 Adds the same multivalued link on 2 DCs and checks we don't end up
316 with 2 copies of the link.
319 # create the link source/target objects
320 src_dn
= self
.unique_dn("CN=src")
321 src_guid
= self
.add_object(self
.ldb_dc1
, src_dn
, objectclass
="group")
322 target_dn
= self
.unique_dn("CN=target")
323 target_guid
= self
.add_object(self
.ldb_dc1
, target_dn
, objectclass
="user")
326 # link the src group to the same target user separately on each DC
327 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
328 self
.ensure_unique_timestamp()
329 self
.add_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
331 self
.sync_DCs(sync_order
=sync_order
)
333 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
334 scope
=SCOPE_BASE
, attrs
=["member"])
335 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
336 scope
=SCOPE_BASE
, attrs
=["member"])
338 # we expect to still have only 1 member in our test group
339 self
.assert_attrs_match(res1
, res2
, "member", 1)
341 def test_duplicate_multi_valued_link(self
):
342 # repeat the test twice, to give each DC a chance to resolve the conflict
343 self
._test
_duplicate
_multi
_valued
_link
(sync_order
=DC1_TO_DC2
)
344 self
._test
_duplicate
_multi
_valued
_link
(sync_order
=DC2_TO_DC1
)
346 def _test_conflict_backlinks(self
, sync_order
):
348 Tests that resolving a source object conflict fixes up any backlinks,
349 e.g. the same user is added to a conflicting group.
352 # create a common link target
353 target_dn
= self
.unique_dn("CN=target")
354 target_guid
= self
.add_object(self
.ldb_dc1
, target_dn
, objectclass
="user")
357 # create the same group (link source) on each DC.
358 # Note that the GUIDs will differ between the DCs
359 src_dn
= self
.unique_dn("CN=src")
360 src1_guid
= self
.add_object(self
.ldb_dc1
, src_dn
, objectclass
="group")
361 self
.ensure_unique_timestamp()
362 src2_guid
= self
.add_object(self
.ldb_dc2
, src_dn
, objectclass
="group")
364 # link the src group to the respective target created
365 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
366 self
.ensure_unique_timestamp()
367 self
.add_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
369 # sync the 2 DCs. We expect the more recent src2 object to win
370 self
.sync_DCs(sync_order
=sync_order
)
372 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % target_guid
,
373 scope
=SCOPE_BASE
, attrs
=["memberOf"])
374 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % target_guid
,
375 scope
=SCOPE_BASE
, attrs
=["memberOf"])
376 src1_backlink
= False
378 # our test user should still be a member of 2 groups (check both DCs agree)
379 self
.assert_attrs_match(res1
, res2
, "memberOf", 2)
381 for val
in res1
[0]["memberOf"]:
382 # check the conflicting object was renamed
383 self
.assertFalse("CNF:%s" % src2_guid
in val
)
384 if "CNF:%s" % src1_guid
in val
:
387 self
.assertTrue(src1_backlink
,
388 "Expected backlink to conflicting source object not found")
390 def test_conflict_backlinks(self
):
391 # repeat the test twice, to give each DC a chance to resolve the conflict
392 self
._test
_conflict
_backlinks
(sync_order
=DC1_TO_DC2
)
393 self
._test
_conflict
_backlinks
(sync_order
=DC2_TO_DC1
)
395 def _test_link_deletion_conflict(self
, sync_order
):
397 Checks that a deleted link conflicting with an active link is
401 # Add the link objects
402 target_dn
= self
.unique_dn("CN=target")
403 target_guid
= self
.add_object(self
.ldb_dc1
, target_dn
, objectclass
="user")
404 src_dn
= self
.unique_dn("CN=src")
405 src_guid
= self
.add_object(self
.ldb_dc1
, src_dn
, objectclass
="group")
408 # add the same link on both DCs, and resolve any conflict
409 self
.add_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
410 self
.ensure_unique_timestamp()
411 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
412 self
.sync_DCs(sync_order
=sync_order
)
414 # delete and re-add the link on one DC
415 self
.del_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
416 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
418 # just delete it on the other DC
419 self
.ensure_unique_timestamp()
420 self
.del_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
421 # sanity-check the link is gone on this DC
422 res1
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
423 scope
=SCOPE_BASE
, attrs
=["member"])
424 self
.assertFalse("member" in res1
[0], "Couldn't delete member attr")
426 # sync the 2 DCs. We expect the more older DC1 attribute to win
427 # because it has a higher version number (even though it's older)
428 self
.sync_DCs(sync_order
=sync_order
)
430 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
431 scope
=SCOPE_BASE
, attrs
=["member"])
432 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
433 scope
=SCOPE_BASE
, attrs
=["member"])
435 # our test user should still be a member of the group (check both DCs agree)
436 self
.assertTrue("member" in res1
[0], "Expected member attribute missing")
437 self
.assert_attrs_match(res1
, res2
, "member", 1)
439 def test_link_deletion_conflict(self
):
440 # repeat the test twice, to give each DC a chance to resolve the conflict
441 self
._test
_link
_deletion
_conflict
(sync_order
=DC1_TO_DC2
)
442 self
._test
_link
_deletion
_conflict
(sync_order
=DC2_TO_DC1
)
444 def _test_obj_deletion_conflict(self
, sync_order
, del_target
):
446 Checks that a receiving a new link for a deleted object gets
450 target_dn
= self
.unique_dn("CN=target")
451 target_guid
= self
.add_object(self
.ldb_dc1
, target_dn
, objectclass
="user")
452 src_dn
= self
.unique_dn("CN=src")
453 src_guid
= self
.add_object(self
.ldb_dc1
, src_dn
, objectclass
="group")
457 # delete the object on one DC
459 search_guid
= src_guid
460 self
.ldb_dc2
.delete(target_dn
)
462 search_guid
= target_guid
463 self
.ldb_dc2
.delete(src_dn
)
465 # add a link on the other DC
466 self
.ensure_unique_timestamp()
467 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
469 self
.sync_DCs(sync_order
=sync_order
)
471 # the object deletion should trump the link addition.
472 # Check the link no longer exists on the remaining object
473 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % search_guid
,
474 scope
=SCOPE_BASE
, attrs
=["member", "memberOf"])
475 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % search_guid
,
476 scope
=SCOPE_BASE
, attrs
=["member", "memberOf"])
478 self
.assertFalse("member" in res1
[0], "member attr shouldn't exist")
479 self
.assertFalse("member" in res2
[0], "member attr shouldn't exist")
480 self
.assertFalse("memberOf" in res1
[0], "member attr shouldn't exist")
481 self
.assertFalse("memberOf" in res2
[0], "member attr shouldn't exist")
483 def test_obj_deletion_conflict(self
):
484 # repeat the test twice, to give each DC a chance to resolve the conflict
485 self
._test
_obj
_deletion
_conflict
(sync_order
=DC1_TO_DC2
, del_target
=True)
486 self
._test
_obj
_deletion
_conflict
(sync_order
=DC2_TO_DC1
, del_target
=True)
488 # and also try deleting the source object instead of the link target
489 self
._test
_obj
_deletion
_conflict
(sync_order
=DC1_TO_DC2
, del_target
=False)
490 self
._test
_obj
_deletion
_conflict
(sync_order
=DC2_TO_DC1
, del_target
=False)
492 def _test_full_sync_link_conflict(self
, sync_order
):
494 Checks that doing a full sync doesn't affect how conflicts get resolved
497 # create the objects for the linked attribute
498 src_dn
= self
.unique_dn("CN=src")
499 src_guid
= self
.add_object(self
.ldb_dc1
, src_dn
, objectclass
="group")
500 target_dn
= self
.unique_dn("CN=target")
501 target1_guid
= self
.add_object(self
.ldb_dc1
, target_dn
, objectclass
="user")
504 # add the same link on both DCs
505 self
.add_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
506 self
.ensure_unique_timestamp()
507 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
509 # Do a couple of full syncs which should resolve the conflict
510 # (but only for one DC)
511 if sync_order
== DC1_TO_DC2
:
512 self
._net
_drs
_replicate
(DC
=self
.dnsname_dc2
, fromDC
=self
.dnsname_dc1
, full_sync
=True)
513 self
._net
_drs
_replicate
(DC
=self
.dnsname_dc2
, fromDC
=self
.dnsname_dc1
, full_sync
=True)
515 self
._net
_drs
_replicate
(DC
=self
.dnsname_dc1
, fromDC
=self
.dnsname_dc2
, full_sync
=True)
516 self
._net
_drs
_replicate
(DC
=self
.dnsname_dc1
, fromDC
=self
.dnsname_dc2
, full_sync
=True)
518 # delete and re-add the link on one DC
519 self
.del_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
520 self
.ensure_unique_timestamp()
521 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
523 # just delete the link on the 2nd DC
524 self
.ensure_unique_timestamp()
525 self
.del_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
527 # sync the 2 DCs. We expect DC1 to win based on version number
528 self
.sync_DCs(sync_order
=sync_order
)
530 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
531 scope
=SCOPE_BASE
, attrs
=["member"])
532 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
533 scope
=SCOPE_BASE
, attrs
=["member"])
535 # check the membership still exits (and both DCs agree)
536 self
.assertTrue("member" in res1
[0], "Expected member attribute missing")
537 self
.assert_attrs_match(res1
, res2
, "member", 1)
539 def test_full_sync_link_conflict(self
):
540 # repeat the test twice, to give each DC a chance to resolve the conflict
541 self
._test
_full
_sync
_link
_conflict
(sync_order
=DC1_TO_DC2
)
542 self
._test
_full
_sync
_link
_conflict
(sync_order
=DC2_TO_DC1
)
544 def _test_conflict_single_valued_link_deleted_winner(self
, sync_order
):
546 Tests a single-value link conflict where the more-up-to-date link value
549 src_ou
= self
.unique_dn("OU=src")
550 src_guid
= self
.add_object(self
.ldb_dc1
, src_ou
)
553 # create a unique target on each DC
554 target1_ou
= self
.unique_dn("OU=target1")
555 target2_ou
= self
.unique_dn("OU=target2")
557 target1_guid
= self
.add_object(self
.ldb_dc1
, target1_ou
)
558 target2_guid
= self
.add_object(self
.ldb_dc2
, target2_ou
)
560 # add the links for the respective targets, and delete one of the links
561 self
.add_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
562 self
.add_link_attr(self
.ldb_dc2
, src_ou
, "managedBy", target2_ou
)
563 self
.ensure_unique_timestamp()
564 self
.del_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
567 self
.sync_DCs(sync_order
=sync_order
)
569 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
570 scope
=SCOPE_BASE
, attrs
=["managedBy"])
571 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
572 scope
=SCOPE_BASE
, attrs
=["managedBy"])
574 # Although the more up-to-date link value is deleted, this shouldn't
575 # trump DC1's active link
576 self
.assert_attrs_match(res1
, res2
, "managedBy", 1)
578 self
.assertTrue(res1
[0]["managedBy"][0] == target2_ou
,
579 "Expected active link win conflict")
581 # we can't query the deleted links over LDAP, but we can check that
582 # the deleted links exist using DRS
583 link1
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
, 0,
584 misc
.GUID(src_guid
), misc
.GUID(target1_guid
))
585 link2
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
,
586 drsuapi
.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
,
587 misc
.GUID(src_guid
), misc
.GUID(target2_guid
))
588 self
._check
_replicated
_links
(src_ou
, [link1
, link2
])
590 def test_conflict_single_valued_link_deleted_winner(self
):
591 # repeat the test twice, to give each DC a chance to resolve the conflict
592 self
._test
_conflict
_single
_valued
_link
_deleted
_winner
(sync_order
=DC1_TO_DC2
)
593 self
._test
_conflict
_single
_valued
_link
_deleted
_winner
(sync_order
=DC2_TO_DC1
)
595 def _test_conflict_single_valued_link_deleted_loser(self
, sync_order
):
597 Tests a single-valued link conflict, where the losing link value is deleted.
599 src_ou
= self
.unique_dn("OU=src")
600 src_guid
= self
.add_object(self
.ldb_dc1
, src_ou
)
603 # create a unique target on each DC
604 target1_ou
= self
.unique_dn("OU=target1")
605 target2_ou
= self
.unique_dn("OU=target2")
607 target1_guid
= self
.add_object(self
.ldb_dc1
, target1_ou
)
608 target2_guid
= self
.add_object(self
.ldb_dc2
, target2_ou
)
610 # add the links - we want the link to end up deleted on DC2, but active on
611 # DC1. DC1 has the better version and DC2 has the better timestamp - the
612 # better version should win
613 self
.add_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
614 self
.del_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
615 self
.add_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
616 self
.ensure_unique_timestamp()
617 self
.add_link_attr(self
.ldb_dc2
, src_ou
, "managedBy", target2_ou
)
618 self
.del_link_attr(self
.ldb_dc2
, src_ou
, "managedBy", target2_ou
)
620 self
.sync_DCs(sync_order
=sync_order
)
622 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
623 scope
=SCOPE_BASE
, attrs
=["managedBy"])
624 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
625 scope
=SCOPE_BASE
, attrs
=["managedBy"])
627 # check the object has only have one occurence of the single-valued
628 # attribute and it matches on both DCs
629 self
.assert_attrs_match(res1
, res2
, "managedBy", 1)
631 self
.assertTrue(res1
[0]["managedBy"][0] == target1_ou
,
632 "Expected most recent update to win conflict")
634 # we can't query the deleted links over LDAP, but we can check DRS
635 # to make sure the DC kept a copy of the conflicting link
636 link1
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
,
637 drsuapi
.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
,
638 misc
.GUID(src_guid
), misc
.GUID(target1_guid
))
639 link2
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
, 0,
640 misc
.GUID(src_guid
), misc
.GUID(target2_guid
))
641 self
._check
_replicated
_links
(src_ou
, [link1
, link2
])
643 def test_conflict_single_valued_link_deleted_loser(self
):
644 # repeat the test twice, to give each DC a chance to resolve the conflict
645 self
._test
_conflict
_single
_valued
_link
_deleted
_loser
(sync_order
=DC1_TO_DC2
)
646 self
._test
_conflict
_single
_valued
_link
_deleted
_loser
(sync_order
=DC2_TO_DC1
)