From d4927a5dc0c45cf1491b6f4080cf482454bc4435 Mon Sep 17 00:00:00 2001 From: Andrew Bartlett Date: Mon, 24 Jul 2023 12:05:18 +1200 Subject: [PATCH] s4-torture/drs: Add test showing that if present in the set the NC root leads and tmp_highest_usn moves The NC root, on any replication when it appears, is the first object to be replicated, including for all subsequent chunks in the replication. However the tmp_highest_usn is not updated by that USN, it must only be updated for the non-NC changes (to match Windows exactly), or at least only updated with the non-NC changes until it would naturally appear. BUG: https://bugzilla.samba.org/show_bug.cgi?id=15401 Signed-off-by: Andrew Bartlett Reviewed-by: Stefan Metzmacher (cherry picked from commit 87414955212143b8502b4c02aca150bc72cb8de5) --- selftest/knownfail.d/getncchanges | 7 +- source4/torture/drs/python/getncchanges.py | 147 +++++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) diff --git a/selftest/knownfail.d/getncchanges b/selftest/knownfail.d/getncchanges index b033b091c72..e9a86ca2ad2 100644 --- a/selftest/knownfail.d/getncchanges +++ b/selftest/knownfail.d/getncchanges @@ -4,4 +4,9 @@ samba4.drs.getncchanges.python\(promoted_dc\).getncchanges.DrsReplicaSyncIntegri samba4.drs.getncchanges.python\(promoted_dc\).getncchanges.DrsReplicaSyncIntegrityTestCase.test_repl_get_tgt_chain\(promoted_dc\) samba4.drs.getncchanges.python\(promoted_dc\).getncchanges.DrsReplicaSyncIntegrityTestCase.test_repl_get_tgt_and_anc\(promoted_dc\) samba4.drs.getncchanges.python\(promoted_dc\).getncchanges.DrsReplicaSyncIntegrityTestCase.test_repl_get_tgt_multivalued_links\(promoted_dc\) -samba4.drs.getncchanges.python\(promoted_dc\).getncchanges.DrsReplicaSyncIntegrityTestCase.test_do_full_repl_mix_no_overlap \ No newline at end of file +samba4.drs.getncchanges.python\(promoted_dc\).getncchanges.DrsReplicaSyncIntegrityTestCase.test_do_full_repl_mix_no_overlap +samba4.drs.getncchanges.python\(.*\).getncchanges.DrsReplicaSyncIntegrityTestCase.test_repl_nc_is_first_nc_change_only\( +samba4.drs.getncchanges.python\(.*\).getncchanges.DrsReplicaSyncIntegrityTestCase.test_repl_nc_is_first\( +samba4.drs.getncchanges.python\(.*\).getncchanges.DrsReplicaSyncIntegrityTestCase.test_repl_nc_is_first_mid\( +samba4.drs.getncchanges.python\(.*\).getncchanges.DrsReplicaSyncIntegrityTestCase.test_repl_nc_is_first_start_zero\( +samba4.drs.getncchanges.python\(.*\).getncchanges.DrsReplicaSyncIntegrityTestCase.test_repl_nc_is_first_start_zero_nc_change\( diff --git a/source4/torture/drs/python/getncchanges.py b/source4/torture/drs/python/getncchanges.py index 5b402e81c88..580d8cc66f3 100644 --- a/source4/torture/drs/python/getncchanges.py +++ b/source4/torture/drs/python/getncchanges.py @@ -1268,6 +1268,153 @@ class DrsReplicaSyncIntegrityTestCase(drs_base.DrsBaseTestCase): """ self._test_do_full_repl_no_overlap(mix=True) + def nc_change(self): + old_base_msg = self.default_conn.ldb_dc.search(base=self.base_dn, + scope=SCOPE_BASE, + attrs=["oEMInformation"]) + rec_cleanup = {"dn": self.base_dn, + "oEMInformation": old_base_msg[0]["oEMInformation"][0]} + m_cleanup = ldb.Message.from_dict(self.default_conn.ldb_dc, + rec_cleanup, + ldb.FLAG_MOD_REPLACE) + + self.addCleanup(self.default_conn.ldb_dc.modify, m_cleanup) + + rec = {"dn": self.base_dn, + "oEMInformation": f"Tortured by Samba's getncchanges.py {self.id()} against {self.default_conn.dnsname_dc}"} + m = ldb.Message.from_dict(self.default_conn.ldb_dc, rec, ldb.FLAG_MOD_REPLACE) + self.default_conn.ldb_dc.modify(m) + + def _test_repl_nc_is_first(self, start_at_zero=True, nc_change=True, ou_change=True, mid_change=False): + """Tests that the NC is always replicated first, but does not move the + tmp_highest_usn at that point, just like 'early' GET_ANC objects. + """ + + # create objects, twice more than the page size of 133 + objs = self.create_object_range(0, 300, prefix="obj") + + if nc_change: + self.nc_change() + + if mid_change: + # create even moire objects + objs = self.create_object_range(301, 450, prefix="obj2") + + base_msg = self.default_conn.ldb_dc.search(base=self.base_dn, + scope=SCOPE_BASE, + attrs=["uSNChanged", + "objectGUID"]) + + base_guid = misc.GUID(base_msg[0]["objectGUID"][0]) + base_usn = int(base_msg[0]["uSNChanged"][0]) + + if ou_change: + # Make one more modification. We want to assert we have + # caught up to the base DN, but Windows both promotes the NC + # to the front and skips including it in the tmp_highest_usn, + # so we make a later modification that will be to show we get + # this change. + rec = {"dn": self.ou, + "postalCode": "0"} + m = ldb.Message.from_dict(self.default_conn.ldb_dc, rec, ldb.FLAG_MOD_REPLACE) + self.default_conn.ldb_dc.modify(m) + + ou_msg = self.default_conn.ldb_dc.search(base=self.ou, + scope=SCOPE_BASE, + attrs=["uSNChanged", + "objectGUID"]) + + ou_guid = misc.GUID(ou_msg[0]["objectGUID"][0]) + ou_usn = int(ou_msg[0]["uSNChanged"][0]) + + # Check some predicates about USN ordering that the below tests will rely on + if ou_change and nc_change: + self.assertGreater(ou_usn, base_usn); + elif not ou_change and nc_change: + self.assertGreater(base_usn, ou_usn); + + ctr6 = self.repl_get_next() + + guid_list_1 = self._get_ctr6_object_guids(ctr6) + if nc_change or start_at_zero: + self.assertEqual(base_guid, misc.GUID(guid_list_1[0])) + self.assertIn(str(base_guid), guid_list_1) + self.assertNotIn(str(base_guid), guid_list_1[1:]) + else: + self.assertNotEqual(base_guid, misc.GUID(guid_list_1[0])) + self.assertNotIn(str(base_guid), guid_list_1) + + self.assertTrue(ctr6.more_data) + + if not ou_change and nc_change: + self.assertLess(ctr6.new_highwatermark.tmp_highest_usn, base_usn) + + i = 0 + while not self.replication_complete(): + i = i + 1 + last_tmp_highest_usn = ctr6.new_highwatermark.tmp_highest_usn + ctr6 = self.repl_get_next() + guid_list_2 = self._get_ctr6_object_guids(ctr6) + if len(guid_list_2) > 0: + self.assertNotEqual(last_tmp_highest_usn, ctr6.new_highwatermark.tmp_highest_usn) + + if (nc_change or start_at_zero) and base_usn > last_tmp_highest_usn: + self.assertEqual(base_guid, misc.GUID(guid_list_2[0]), + f"pass={i} more_data={ctr6.more_data} base_usn={base_usn} tmp_highest_usn={ctr6.new_highwatermark.tmp_highest_usn} last_tmp_highest_usn={last_tmp_highest_usn}") + self.assertIn(str(base_guid), guid_list_2, + f"pass {i}·more_data={ctr6.more_data} base_usn={base_usn} tmp_highest_usn={ctr6.new_highwatermark.tmp_highest_usn} last_tmp_highest_usn={last_tmp_highest_usn}") + else: + self.assertNotIn(str(base_guid), guid_list_2, + f"pass {i}·more_data={ctr6.more_data} base_usn={base_usn} tmp_highest_usn={ctr6.new_highwatermark.tmp_highest_usn} last_tmp_highest_usn={last_tmp_highest_usn}") + + if ou_change: + # The modification to the base OU should be in the final chunk + self.assertIn(str(ou_guid), guid_list_2) + self.assertGreaterEqual(ctr6.new_highwatermark.highest_usn, + ou_usn) + else: + # Show that the NC root change does not show up in the + # highest_usn. We either get the change before or after + # it. + self.assertNotEqual(ctr6.new_highwatermark.highest_usn, + base_usn) + self.assertEqual(ctr6.new_highwatermark.highest_usn, + ctr6.new_highwatermark.tmp_highest_usn) + + self.assertFalse(ctr6.more_data) + + def test_repl_nc_is_first_start_zero_nc_change(self): + self.default_hwm = drsuapi.DsReplicaHighWaterMark() + self._test_repl_nc_is_first(start_at_zero=True, nc_change=True, ou_change=True) + + def test_repl_nc_is_first_start_zero(self): + # Get the NC change in the middle of the replication stream, certainly not at the start or end + self.nc_change() + self.default_hwm = drsuapi.DsReplicaHighWaterMark() + self._test_repl_nc_is_first(start_at_zero=True, nc_change=False, ou_change=False) + + def test_repl_nc_is_first_mid(self): + # This is a modification of the next test, that Samba + # will pass as it will always include the NC in the + # tmp_highest_usn at the point where it belongs + self._test_repl_nc_is_first(start_at_zero=False, + nc_change=True, + ou_change=True, + mid_change=True) + + def test_repl_nc_is_first(self): + # This is a modification of the next test, that Samba + # will pass as it will always include the NC in the + # tmp_highest_usn at the point where it belongs + self._test_repl_nc_is_first(start_at_zero=False, nc_change=True, ou_change=True) + + def test_repl_nc_is_first_nc_change_only(self): + # This shows that the NC change is not reflected in the tmp_highest_usn + self._test_repl_nc_is_first(start_at_zero=False, nc_change=True, ou_change=False) + + def test_repl_nc_is_first_no_change(self): + # The NC should not be present in this replication + self._test_repl_nc_is_first(start_at_zero=False, nc_change=False, ou_change=False) class DcConnection: """Helper class to track a connection to another DC""" -- 2.34.1