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 \
29 # link_conflicts -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD"
35 from ldb
import SCOPE_BASE
39 from drs_base
import AbstractLink
40 from samba
.dcerpc
import drsuapi
, misc
41 from samba
.dcerpc
.drsuapi
import DRSUAPI_EXOP_ERR_SUCCESS
43 # specifies the order to sync DCs in
48 class DrsReplicaLinkConflictTestCase(drs_base
.DrsBaseTestCase
):
50 super(DrsReplicaLinkConflictTestCase
, self
).setUp()
52 self
.ou
= samba
.tests
.create_test_ou(self
.ldb_dc1
,
54 self
.base_dn
= self
.ldb_dc1
.get_default_basedn()
56 (self
.drs
, self
.drs_handle
) = self
._ds
_bind
(self
.dnsname_dc1
)
57 (self
.drs2
, self
.drs2_handle
) = self
._ds
_bind
(self
.dnsname_dc2
)
59 # disable replication for the tests so we can control at what point
60 # the DCs try to replicate
61 self
._disable
_inbound
_repl
(self
.dnsname_dc1
)
62 self
._disable
_inbound
_repl
(self
.dnsname_dc2
)
65 # re-enable replication
66 self
._enable
_inbound
_repl
(self
.dnsname_dc1
)
67 self
._enable
_inbound
_repl
(self
.dnsname_dc2
)
68 self
.ldb_dc1
.delete(self
.ou
, ["tree_delete:1"])
69 super(DrsReplicaLinkConflictTestCase
, self
).tearDown()
71 def get_guid(self
, samdb
, dn
):
72 """Returns an object's GUID (in string format)"""
73 res
= samdb
.search(base
=dn
, attrs
=["objectGUID"], scope
=ldb
.SCOPE_BASE
)
74 return self
._GUID
_string
(res
[0]['objectGUID'][0])
76 def add_object(self
, samdb
, dn
, objectclass
="organizationalunit"):
78 samdb
.add({"dn": dn
, "objectclass": objectclass
})
79 return self
.get_guid(samdb
, dn
)
81 def modify_object(self
, samdb
, dn
, attr
, value
):
82 """Modifies an attribute for an object"""
84 m
.dn
= ldb
.Dn(samdb
, dn
)
85 m
[attr
] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_ADD
, attr
)
88 def add_link_attr(self
, samdb
, source_dn
, attr
, target_dn
):
89 """Adds a linked attribute between 2 objects"""
90 # add the specified attribute to the source object
91 self
.modify_object(samdb
, source_dn
, attr
, target_dn
)
93 def del_link_attr(self
, samdb
, src
, attr
, target
):
95 m
.dn
= ldb
.Dn(samdb
, src
)
96 m
[attr
] = ldb
.MessageElement(target
, ldb
.FLAG_MOD_DELETE
, attr
)
99 def sync_DCs(self
, sync_order
=DC1_TO_DC2
):
100 """Manually syncs the 2 DCs to ensure they're in sync"""
101 if sync_order
== DC1_TO_DC2
:
102 # sync DC1-->DC2, then DC2-->DC1
103 self
._net
_drs
_replicate
(DC
=self
.dnsname_dc2
,
104 fromDC
=self
.dnsname_dc1
)
105 self
._net
_drs
_replicate
(DC
=self
.dnsname_dc1
,
106 fromDC
=self
.dnsname_dc2
)
108 # sync DC2-->DC1, then DC1-->DC2
109 self
._net
_drs
_replicate
(DC
=self
.dnsname_dc1
,
110 fromDC
=self
.dnsname_dc2
)
111 self
._net
_drs
_replicate
(DC
=self
.dnsname_dc2
,
112 fromDC
=self
.dnsname_dc1
)
114 def ensure_unique_timestamp(self
):
115 """Waits a second to ensure a unique timestamp between 2 objects"""
118 def unique_dn(self
, obj_name
):
119 """Returns a unique object DN"""
120 # Because we run each test case twice, we need to create a unique DN so
121 # that the 2nd run doesn't hit objects that already exist. Add some
122 # randomness to the object DN to make it unique
123 rand
= random
.randint(1, 10000000)
124 return "%s-%d,%s" % (obj_name
, rand
, self
.ou
)
126 def assert_attrs_match(self
, res1
, res2
, attr
, expected_count
):
128 Asserts that the search results contain the expected number of
129 attributes and the results match on both DCs
131 actual_len
= len(res1
[0][attr
])
132 self
.assertTrue(actual_len
== expected_count
,
133 "Expected %u %s attributes, got %u" % (expected_count
,
136 actual_len
= len(res2
[0][attr
])
137 self
.assertTrue(actual_len
== expected_count
,
138 "Expected %u %s attributes, got %u" % (expected_count
,
142 # check DCs both agree on the same linked attributes
143 for val
in res1
[0][attr
]:
144 self
.assertTrue(val
in res2
[0][attr
],
145 "%s '%s' not found on DC2" % (attr
, val
))
147 def zero_highwatermark(self
):
148 """Returns a zeroed highwatermark so that all DRS data gets returned"""
149 hwm
= drsuapi
.DsReplicaHighWaterMark()
150 hwm
.tmp_highest_usn
= 0
155 def _check_replicated_links(self
, src_obj_dn
, expected_links
):
156 """Checks that replication sends back the expected linked attributes"""
157 self
._check
_replication
([src_obj_dn
],
158 drsuapi
.DRSUAPI_DRS_WRIT_REP
,
160 drs_error
=drsuapi
.DRSUAPI_EXOP_ERR_SUCCESS
,
161 nc_dn_str
=src_obj_dn
,
162 exop
=drsuapi
.DRSUAPI_EXOP_REPL_OBJ
,
163 expected_links
=expected_links
,
164 highwatermark
=self
.zero_highwatermark())
167 self
.set_test_ldb_dc(self
.ldb_dc2
)
169 self
._check
_replication
([src_obj_dn
],
170 drsuapi
.DRSUAPI_DRS_WRIT_REP
,
172 drs_error
=drsuapi
.DRSUAPI_EXOP_ERR_SUCCESS
,
173 nc_dn_str
=src_obj_dn
,
174 exop
=drsuapi
.DRSUAPI_EXOP_REPL_OBJ
,
175 expected_links
=expected_links
,
176 highwatermark
=self
.zero_highwatermark(),
177 drs
=self
.drs2
, drs_handle
=self
.drs2_handle
)
178 self
.set_test_ldb_dc(self
.ldb_dc1
)
180 def _test_conflict_single_valued_link(self
, sync_order
):
182 Tests a simple single-value link conflict, i.e. each DC adds a link to
183 the same source object but linking to different targets.
185 src_ou
= self
.unique_dn("OU=src")
186 src_guid
= self
.add_object(self
.ldb_dc1
, src_ou
)
189 # create a unique target on each DC
190 target1_ou
= self
.unique_dn("OU=target1")
191 target2_ou
= self
.unique_dn("OU=target2")
193 target1_guid
= self
.add_object(self
.ldb_dc1
, target1_ou
)
194 target2_guid
= self
.add_object(self
.ldb_dc2
, target2_ou
)
196 # link the test OU to the respective targets created
197 self
.add_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
198 self
.ensure_unique_timestamp()
199 self
.add_link_attr(self
.ldb_dc2
, src_ou
, "managedBy", target2_ou
)
202 self
.sync_DCs(sync_order
=sync_order
)
204 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
205 scope
=SCOPE_BASE
, attrs
=["managedBy"])
206 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
207 scope
=SCOPE_BASE
, attrs
=["managedBy"])
209 # check the object has only have one occurence of the single-valued
210 # attribute and it matches on both DCs
211 self
.assert_attrs_match(res1
, res2
, "managedBy", 1)
213 self
.assertTrue(str(res1
[0]["managedBy"][0]) == target2_ou
,
214 "Expected most recent update to win conflict")
216 # we can't query the deleted links over LDAP, but we can check DRS
217 # to make sure the DC kept a copy of the conflicting link
218 link1
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
, 0,
219 misc
.GUID(src_guid
), misc
.GUID(target1_guid
))
220 link2
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
,
221 drsuapi
.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
,
222 misc
.GUID(src_guid
), misc
.GUID(target2_guid
))
223 self
._check
_replicated
_links
(src_ou
, [link1
, link2
])
225 def test_conflict_single_valued_link(self
):
226 # repeat the test twice, to give each DC a chance to resolve
228 self
._test
_conflict
_single
_valued
_link
(sync_order
=DC1_TO_DC2
)
229 self
._test
_conflict
_single
_valued
_link
(sync_order
=DC2_TO_DC1
)
231 def _test_duplicate_single_valued_link(self
, sync_order
):
233 Adds the same single-valued link on 2 DCs and checks we don't end up
234 with 2 copies of the link.
236 # create unique objects for the link
237 target_ou
= self
.unique_dn("OU=target")
238 self
.add_object(self
.ldb_dc1
, target_ou
)
239 src_ou
= self
.unique_dn("OU=src")
240 src_guid
= self
.add_object(self
.ldb_dc1
, src_ou
)
243 # link the same test OU to the same target on both DCs
244 self
.add_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target_ou
)
245 self
.ensure_unique_timestamp()
246 self
.add_link_attr(self
.ldb_dc2
, src_ou
, "managedBy", target_ou
)
249 self
.sync_DCs(sync_order
=sync_order
)
251 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
252 scope
=SCOPE_BASE
, attrs
=["managedBy"])
253 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
254 scope
=SCOPE_BASE
, attrs
=["managedBy"])
256 # check the object has only have one occurence of the single-valued
257 # attribute and it matches on both DCs
258 self
.assert_attrs_match(res1
, res2
, "managedBy", 1)
260 def test_duplicate_single_valued_link(self
):
261 # repeat the test twice, to give each DC a chance to resolve
263 self
._test
_duplicate
_single
_valued
_link
(sync_order
=DC1_TO_DC2
)
264 self
._test
_duplicate
_single
_valued
_link
(sync_order
=DC2_TO_DC1
)
266 def _test_conflict_multi_valued_link(self
, sync_order
):
268 Tests a simple multi-valued link conflict. This adds 2 objects with the
269 same username on 2 different DCs and checks their group membership is
270 preserved after the conflict is resolved.
273 # create a common link source
274 src_dn
= self
.unique_dn("CN=src")
275 src_guid
= self
.add_object(self
.ldb_dc1
, src_dn
, objectclass
="group")
278 # create the same user (link target) on each DC.
279 # Note that the GUIDs will differ between the DCs
280 target_dn
= self
.unique_dn("CN=target")
281 target1_guid
= self
.add_object(self
.ldb_dc1
, target_dn
,
283 self
.ensure_unique_timestamp()
284 target2_guid
= self
.add_object(self
.ldb_dc2
, target_dn
,
287 # link the src group to the respective target created
288 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
289 self
.ensure_unique_timestamp()
290 self
.add_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
292 # sync the 2 DCs. We expect the more recent target2 object to win
293 self
.sync_DCs(sync_order
=sync_order
)
295 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
296 scope
=SCOPE_BASE
, attrs
=["member"])
297 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
298 scope
=SCOPE_BASE
, attrs
=["member"])
299 target1_conflict
= False
301 # we expect exactly 2 members in our test group (both DCs should agree)
302 self
.assert_attrs_match(res1
, res2
, "member", 2)
304 for val
in [str(val
) for val
in res1
[0]["member"]]:
305 # check the expected conflicting object was renamed
306 self
.assertFalse("CNF:%s" % target2_guid
in val
)
307 if "CNF:%s" % target1_guid
in val
:
308 target1_conflict
= True
310 self
.assertTrue(target1_conflict
,
311 "Expected link to conflicting target object not found")
313 def test_conflict_multi_valued_link(self
):
314 # repeat the test twice, to give each DC a chance to resolve
316 self
._test
_conflict
_multi
_valued
_link
(sync_order
=DC1_TO_DC2
)
317 self
._test
_conflict
_multi
_valued
_link
(sync_order
=DC2_TO_DC1
)
319 def _test_duplicate_multi_valued_link(self
, sync_order
):
321 Adds the same multivalued link on 2 DCs and checks we don't end up
322 with 2 copies of the link.
325 # create the link source/target objects
326 src_dn
= self
.unique_dn("CN=src")
327 src_guid
= self
.add_object(self
.ldb_dc1
, src_dn
, objectclass
="group")
328 target_dn
= self
.unique_dn("CN=target")
329 self
.add_object(self
.ldb_dc1
, target_dn
, objectclass
="user")
332 # link the src group to the same target user separately on each DC
333 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
334 self
.ensure_unique_timestamp()
335 self
.add_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
337 self
.sync_DCs(sync_order
=sync_order
)
339 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
340 scope
=SCOPE_BASE
, attrs
=["member"])
341 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
342 scope
=SCOPE_BASE
, attrs
=["member"])
344 # we expect to still have only 1 member in our test group
345 self
.assert_attrs_match(res1
, res2
, "member", 1)
347 def test_duplicate_multi_valued_link(self
):
348 # repeat the test twice, to give each DC a chance to resolve
350 self
._test
_duplicate
_multi
_valued
_link
(sync_order
=DC1_TO_DC2
)
351 self
._test
_duplicate
_multi
_valued
_link
(sync_order
=DC2_TO_DC1
)
353 def _test_conflict_backlinks(self
, sync_order
):
355 Tests that resolving a source object conflict fixes up any backlinks,
356 e.g. the same user is added to a conflicting group.
359 # create a common link target
360 target_dn
= self
.unique_dn("CN=target")
361 target_guid
= self
.add_object(self
.ldb_dc1
, target_dn
,
365 # create the same group (link source) on each DC.
366 # Note that the GUIDs will differ between the DCs
367 src_dn
= self
.unique_dn("CN=src")
368 src1_guid
= self
.add_object(self
.ldb_dc1
, src_dn
, objectclass
="group")
369 self
.ensure_unique_timestamp()
370 src2_guid
= self
.add_object(self
.ldb_dc2
, src_dn
, objectclass
="group")
372 # link the src group to the respective target created
373 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
374 self
.ensure_unique_timestamp()
375 self
.add_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
377 # sync the 2 DCs. We expect the more recent src2 object to win
378 self
.sync_DCs(sync_order
=sync_order
)
380 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % target_guid
,
381 scope
=SCOPE_BASE
, attrs
=["memberOf"])
382 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % target_guid
,
383 scope
=SCOPE_BASE
, attrs
=["memberOf"])
384 src1_backlink
= False
386 # our test user should still be a member of 2 groups (check both
388 self
.assert_attrs_match(res1
, res2
, "memberOf", 2)
390 for val
in [str(val
) for val
in res1
[0]["memberOf"]]:
391 # check the conflicting object was renamed
392 self
.assertFalse("CNF:%s" % src2_guid
in val
)
393 if "CNF:%s" % src1_guid
in val
:
396 self
.assertTrue(src1_backlink
,
397 "Backlink to conflicting source object not found")
399 def test_conflict_backlinks(self
):
400 # repeat the test twice, to give each DC a chance to resolve
402 self
._test
_conflict
_backlinks
(sync_order
=DC1_TO_DC2
)
403 self
._test
_conflict
_backlinks
(sync_order
=DC2_TO_DC1
)
405 def _test_link_deletion_conflict(self
, sync_order
):
407 Checks that a deleted link conflicting with an active link is
411 # Add the link objects
412 target_dn
= self
.unique_dn("CN=target")
413 self
.add_object(self
.ldb_dc1
, target_dn
, objectclass
="user")
414 src_dn
= self
.unique_dn("CN=src")
415 src_guid
= self
.add_object(self
.ldb_dc1
, src_dn
, objectclass
="group")
418 # add the same link on both DCs, and resolve any conflict
419 self
.add_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
420 self
.ensure_unique_timestamp()
421 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
422 self
.sync_DCs(sync_order
=sync_order
)
424 # delete and re-add the link on one DC
425 self
.del_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
426 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
428 # just delete it on the other DC
429 self
.ensure_unique_timestamp()
430 self
.del_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
431 # sanity-check the link is gone on this DC
432 res1
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
433 scope
=SCOPE_BASE
, attrs
=["member"])
434 self
.assertFalse("member" in res1
[0], "Couldn't delete member attr")
436 # sync the 2 DCs. We expect the more older DC1 attribute to win
437 # because it has a higher version number (even though it's older)
438 self
.sync_DCs(sync_order
=sync_order
)
440 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
441 scope
=SCOPE_BASE
, attrs
=["member"])
442 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
443 scope
=SCOPE_BASE
, attrs
=["member"])
445 # our test user should still be a member of the group (check both
447 self
.assertTrue("member" in res1
[0],
448 "Expected member attribute missing")
449 self
.assert_attrs_match(res1
, res2
, "member", 1)
451 def test_link_deletion_conflict(self
):
452 # repeat the test twice, to give each DC a chance to resolve
454 self
._test
_link
_deletion
_conflict
(sync_order
=DC1_TO_DC2
)
455 self
._test
_link
_deletion
_conflict
(sync_order
=DC2_TO_DC1
)
457 def _test_obj_deletion_conflict(self
, sync_order
, del_target
):
459 Checks that a receiving a new link for a deleted object gets
463 target_dn
= self
.unique_dn("CN=target")
464 target_guid
= self
.add_object(self
.ldb_dc1
, target_dn
,
466 src_dn
= self
.unique_dn("CN=src")
467 src_guid
= self
.add_object(self
.ldb_dc1
, src_dn
, objectclass
="group")
471 # delete the object on one DC
473 search_guid
= src_guid
474 self
.ldb_dc2
.delete(target_dn
)
476 search_guid
= target_guid
477 self
.ldb_dc2
.delete(src_dn
)
479 # add a link on the other DC
480 self
.ensure_unique_timestamp()
481 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
483 self
.sync_DCs(sync_order
=sync_order
)
485 # the object deletion should trump the link addition.
486 # Check the link no longer exists on the remaining object
487 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % search_guid
,
489 attrs
=["member", "memberOf"])
490 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % search_guid
,
492 attrs
=["member", "memberOf"])
494 self
.assertFalse("member" in res1
[0], "member attr shouldn't exist")
495 self
.assertFalse("member" in res2
[0], "member attr shouldn't exist")
496 self
.assertFalse("memberOf" in res1
[0], "member attr shouldn't exist")
497 self
.assertFalse("memberOf" in res2
[0], "member attr shouldn't exist")
499 def test_obj_deletion_conflict(self
):
500 # repeat the test twice, to give each DC a chance to resolve
502 self
._test
_obj
_deletion
_conflict
(sync_order
=DC1_TO_DC2
,
504 self
._test
_obj
_deletion
_conflict
(sync_order
=DC2_TO_DC1
,
507 # and also try deleting the source object instead of the link target
508 self
._test
_obj
_deletion
_conflict
(sync_order
=DC1_TO_DC2
,
510 self
._test
_obj
_deletion
_conflict
(sync_order
=DC2_TO_DC1
,
513 def _test_full_sync_link_conflict(self
, sync_order
):
515 Checks that doing a full sync doesn't affect how conflicts get resolved
518 # create the objects for the linked attribute
519 src_dn
= self
.unique_dn("CN=src")
520 src_guid
= self
.add_object(self
.ldb_dc1
, src_dn
, objectclass
="group")
521 target_dn
= self
.unique_dn("CN=target")
522 self
.add_object(self
.ldb_dc1
, target_dn
, objectclass
="user")
525 # add the same link on both DCs
526 self
.add_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
527 self
.ensure_unique_timestamp()
528 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
530 # Do a couple of full syncs which should resolve the conflict
531 # (but only for one DC)
532 if sync_order
== DC1_TO_DC2
:
533 self
._net
_drs
_replicate
(DC
=self
.dnsname_dc2
,
534 fromDC
=self
.dnsname_dc1
,
536 self
._net
_drs
_replicate
(DC
=self
.dnsname_dc2
,
537 fromDC
=self
.dnsname_dc1
,
540 self
._net
_drs
_replicate
(DC
=self
.dnsname_dc1
,
541 fromDC
=self
.dnsname_dc2
,
543 self
._net
_drs
_replicate
(DC
=self
.dnsname_dc1
,
544 fromDC
=self
.dnsname_dc2
,
547 # delete and re-add the link on one DC
548 self
.del_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
549 self
.ensure_unique_timestamp()
550 self
.add_link_attr(self
.ldb_dc1
, src_dn
, "member", target_dn
)
552 # just delete the link on the 2nd DC
553 self
.ensure_unique_timestamp()
554 self
.del_link_attr(self
.ldb_dc2
, src_dn
, "member", target_dn
)
556 # sync the 2 DCs. We expect DC1 to win based on version number
557 self
.sync_DCs(sync_order
=sync_order
)
559 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
560 scope
=SCOPE_BASE
, attrs
=["member"])
561 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
562 scope
=SCOPE_BASE
, attrs
=["member"])
564 # check the membership still exits (and both DCs agree)
565 self
.assertTrue("member" in res1
[0],
566 "Expected member attribute missing")
567 self
.assert_attrs_match(res1
, res2
, "member", 1)
569 def test_full_sync_link_conflict(self
):
570 # repeat the test twice, to give each DC a chance to resolve
572 self
._test
_full
_sync
_link
_conflict
(sync_order
=DC1_TO_DC2
)
573 self
._test
_full
_sync
_link
_conflict
(sync_order
=DC2_TO_DC1
)
575 def _singleval_link_conflict_deleted_winner(self
, sync_order
):
577 Tests a single-value link conflict where the more-up-to-date link value
580 src_ou
= self
.unique_dn("OU=src")
581 src_guid
= self
.add_object(self
.ldb_dc1
, src_ou
)
584 # create a unique target on each DC
585 target1_ou
= self
.unique_dn("OU=target1")
586 target2_ou
= self
.unique_dn("OU=target2")
588 target1_guid
= self
.add_object(self
.ldb_dc1
, target1_ou
)
589 target2_guid
= self
.add_object(self
.ldb_dc2
, target2_ou
)
591 # add the links for the respective targets, and delete one of the links
592 self
.add_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
593 self
.add_link_attr(self
.ldb_dc2
, src_ou
, "managedBy", target2_ou
)
594 self
.ensure_unique_timestamp()
595 self
.del_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
598 self
.sync_DCs(sync_order
=sync_order
)
600 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
601 scope
=SCOPE_BASE
, attrs
=["managedBy"])
602 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
603 scope
=SCOPE_BASE
, attrs
=["managedBy"])
605 # Although the more up-to-date link value is deleted, this shouldn't
606 # trump DC1's active link
607 self
.assert_attrs_match(res1
, res2
, "managedBy", 1)
609 self
.assertTrue(str(res1
[0]["managedBy"][0]) == target2_ou
,
610 "Expected active link win conflict")
612 # we can't query the deleted links over LDAP, but we can check that
613 # the deleted links exist using DRS
614 link1
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
, 0,
615 misc
.GUID(src_guid
), misc
.GUID(target1_guid
))
616 link2
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
,
617 drsuapi
.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
,
618 misc
.GUID(src_guid
), misc
.GUID(target2_guid
))
619 self
._check
_replicated
_links
(src_ou
, [link1
, link2
])
621 def test_conflict_single_valued_link_deleted_winner(self
):
622 # repeat the test twice, to give each DC a chance to resolve
624 self
._singleval
_link
_conflict
_deleted
_winner
(sync_order
=DC1_TO_DC2
)
625 self
._singleval
_link
_conflict
_deleted
_winner
(sync_order
=DC2_TO_DC1
)
627 def _singleval_link_conflict_deleted_loser(self
, sync_order
):
629 Tests a single-valued link conflict, where the losing link value is
632 src_ou
= self
.unique_dn("OU=src")
633 src_guid
= self
.add_object(self
.ldb_dc1
, src_ou
)
636 # create a unique target on each DC
637 target1_ou
= self
.unique_dn("OU=target1")
638 target2_ou
= self
.unique_dn("OU=target2")
640 target1_guid
= self
.add_object(self
.ldb_dc1
, target1_ou
)
641 target2_guid
= self
.add_object(self
.ldb_dc2
, target2_ou
)
643 # add the links - we want the link to end up deleted on DC2, but active
644 # on DC1. DC1 has the better version and DC2 has the better timestamp -
645 # the better version should win
646 self
.add_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
647 self
.del_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
648 self
.add_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
649 self
.ensure_unique_timestamp()
650 self
.add_link_attr(self
.ldb_dc2
, src_ou
, "managedBy", target2_ou
)
651 self
.del_link_attr(self
.ldb_dc2
, src_ou
, "managedBy", target2_ou
)
653 self
.sync_DCs(sync_order
=sync_order
)
655 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
656 scope
=SCOPE_BASE
, attrs
=["managedBy"])
657 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
658 scope
=SCOPE_BASE
, attrs
=["managedBy"])
660 # check the object has only have one occurence of the single-valued
661 # attribute and it matches on both DCs
662 self
.assert_attrs_match(res1
, res2
, "managedBy", 1)
664 self
.assertTrue(str(res1
[0]["managedBy"][0]) == target1_ou
,
665 "Expected most recent update to win conflict")
667 # we can't query the deleted links over LDAP, but we can check DRS
668 # to make sure the DC kept a copy of the conflicting link
669 link1
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
,
670 drsuapi
.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
,
671 misc
.GUID(src_guid
), misc
.GUID(target1_guid
))
672 link2
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
, 0,
673 misc
.GUID(src_guid
), misc
.GUID(target2_guid
))
674 self
._check
_replicated
_links
(src_ou
, [link1
, link2
])
676 def test_conflict_single_valued_link_deleted_loser(self
):
677 # repeat the test twice, to give each DC a chance to resolve
679 self
._singleval
_link
_conflict
_deleted
_loser
(sync_order
=DC1_TO_DC2
)
680 self
._singleval
_link
_conflict
_deleted
_loser
(sync_order
=DC2_TO_DC1
)
682 def _test_conflict_existing_single_valued_link(self
, sync_order
):
684 Tests a single-valued link conflict, where the conflicting link value
685 already exists (as inactive) on both DCs.
687 # create the link objects
688 src_ou
= self
.unique_dn("OU=src")
689 src_guid
= self
.add_object(self
.ldb_dc1
, src_ou
)
691 target1_ou
= self
.unique_dn("OU=target1")
692 target2_ou
= self
.unique_dn("OU=target2")
693 target1_guid
= self
.add_object(self
.ldb_dc1
, target1_ou
)
694 target2_guid
= self
.add_object(self
.ldb_dc1
, target2_ou
)
696 # add the links, but then delete them
697 self
.add_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
698 self
.del_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
699 self
.add_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target2_ou
)
700 self
.del_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target2_ou
)
703 # re-add the links independently on each DC
704 self
.add_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
705 self
.ensure_unique_timestamp()
706 self
.add_link_attr(self
.ldb_dc2
, src_ou
, "managedBy", target2_ou
)
708 # try to sync the 2 DCs
709 self
.sync_DCs(sync_order
=sync_order
)
711 res1
= self
.ldb_dc1
.search(base
="<GUID=%s>" % src_guid
,
712 scope
=SCOPE_BASE
, attrs
=["managedBy"])
713 res2
= self
.ldb_dc2
.search(base
="<GUID=%s>" % src_guid
,
714 scope
=SCOPE_BASE
, attrs
=["managedBy"])
716 # check the object has only have one occurence of the single-valued
717 # attribute and it matches on both DCs
718 self
.assert_attrs_match(res1
, res2
, "managedBy", 1)
720 # here we expect DC2 to win because it has the more recent link
721 self
.assertTrue(str(res1
[0]["managedBy"][0]) == target2_ou
,
722 "Expected most recent update to win conflict")
724 # we can't query the deleted links over LDAP, but we can check DRS
725 # to make sure the DC kept a copy of the conflicting link
726 link1
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
, 0,
727 misc
.GUID(src_guid
), misc
.GUID(target1_guid
))
728 link2
= AbstractLink(drsuapi
.DRSUAPI_ATTID_managedBy
,
729 drsuapi
.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
,
730 misc
.GUID(src_guid
), misc
.GUID(target2_guid
))
731 self
._check
_replicated
_links
(src_ou
, [link1
, link2
])
733 def test_conflict_existing_single_valued_link(self
):
734 # repeat the test twice, to give each DC a chance to resolve
736 self
._test
_conflict
_existing
_single
_valued
_link
(sync_order
=DC1_TO_DC2
)
737 self
._test
_conflict
_existing
_single
_valued
_link
(sync_order
=DC2_TO_DC1
)
739 def test_link_attr_version(self
):
741 Checks the link attribute version starts from the correct value
743 # create some objects and add a link
744 src_ou
= self
.unique_dn("OU=src")
745 self
.add_object(self
.ldb_dc1
, src_ou
)
746 target1_ou
= self
.unique_dn("OU=target1")
747 self
.add_object(self
.ldb_dc1
, target1_ou
)
748 self
.add_link_attr(self
.ldb_dc1
, src_ou
, "managedBy", target1_ou
)
750 # get the link info via replication
751 ctr6
= self
._get
_replication
(drsuapi
.DRSUAPI_DRS_WRIT_REP
,
753 drs_error
=DRSUAPI_EXOP_ERR_SUCCESS
,
754 exop
=drsuapi
.DRSUAPI_EXOP_REPL_OBJ
,
755 highwatermark
=self
.zero_highwatermark(),
758 self
.assertTrue(ctr6
.linked_attributes_count
== 1,
759 "DRS didn't return a link")
760 link
= ctr6
.linked_attributes
[0]
761 rcvd_version
= link
.meta_data
.version
762 self
.assertTrue(rcvd_version
== 1,
763 "Link version started from %u, not 1" % rcvd_version
)