getncchanges.py: Add GET_ANC replication test case
[nivanova/samba-autobuild/.git] / source4 / torture / drs / python / getncchanges.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Tests various schema replication scenarios
5 #
6 # Copyright (C) Catalyst.Net Ltd. 2017
7 #
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21
22 #
23 # Usage:
24 #  export DC1=dc1_dns_name
25 #  export DC2=dc2_dns_name
26 #  export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun
27 #  PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN getncchanges -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD"
28 #
29
30 import drs_base
31 import samba.tests
32 import ldb
33 from ldb import SCOPE_BASE
34
35 from samba.dcerpc import drsuapi
36
37 class DrsReplicaSyncIntegrityTestCase(drs_base.DrsBaseTestCase):
38     def setUp(self):
39         super(DrsReplicaSyncIntegrityTestCase, self).setUp()
40         self.base_dn = self.ldb_dc1.get_default_basedn()
41         self.ou = "OU=uptodateness_test,%s" % self.base_dn
42         self.ldb_dc1.add({
43             "dn": self.ou,
44             "objectclass": "organizationalUnit"})
45         (self.drs, self.drs_handle) = self._ds_bind(self.dnsname_dc1)
46         (self.default_hwm, self.default_utdv) = self._get_highest_hwm_utdv(self.ldb_dc1)
47
48         # 100 is the minimum max_objects that Microsoft seems to honour
49         # (the max honoured is 400ish), so we use that in these tests
50         self.max_objects = 100
51         self.last_ctr = None
52
53         # store whether we used GET_ANC flags in the requests
54         self.used_get_anc = False
55
56     def tearDown(self):
57         super(DrsReplicaSyncIntegrityTestCase, self).tearDown()
58         # tidyup groups and users
59         try:
60             self.ldb_dc1.delete(self.ou, ["tree_delete:1"])
61         except ldb.LdbError as (enum, string):
62             if enum == ldb.ERR_NO_SUCH_OBJECT:
63                 pass
64
65     def add_object(self, dn):
66         """Adds an OU object"""
67         self.ldb_dc1.add({"dn": dn, "objectclass": "organizationalunit"})
68         res = self.ldb_dc1.search(base=dn, scope=SCOPE_BASE)
69         self.assertEquals(len(res), 1)
70
71     def modify_object(self, dn, attr, value):
72         """Modifies an object's USN by adding an attribute value to it"""
73         m = ldb.Message()
74         m.dn = ldb.Dn(self.ldb_dc1, dn)
75         m[attr] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, attr)
76         self.ldb_dc1.modify(m)
77
78     def create_object_range(self, start, end, prefix="",
79                             children=None, parent_list=None):
80         """
81         Creates a block of objects. Object names are numbered sequentially,
82         using the optional prefix supplied. If the children parameter is
83         supplied it will create a parent-child hierarchy and return the
84         top-level parents separately.
85         """
86         dn_list = []
87
88         # Use dummy/empty lists if we're not creating a parent/child hierarchy
89         if children is None:
90             children = []
91
92         if parent_list is None:
93             parent_list = []
94
95         # Create the parents first, then the children.
96         # This makes it easier to see in debug when GET_ANC takes effect
97         # because the parent/children become interleaved (by default,
98         # this approach means the objects are organized into blocks of
99         # parents and blocks of children together)
100         for x in range(start, end):
101             ou = "OU=test_ou_%s%d,%s" % (prefix, x, self.ou)
102             self.add_object(ou)
103             dn_list.append(ou)
104
105             # keep track of the top-level parents (if needed)
106             parent_list.append(ou)
107
108         # create the block of children (if needed)
109         for x in range(start, end):
110             for child in children:
111                 ou = "OU=test_ou_child%s%d,%s" % (child, x, parent_list[x])
112                 self.add_object(ou)
113                 dn_list.append(ou)
114
115         return dn_list
116
117     def assert_expected_data(self, received_list, expected_list):
118         """
119         Asserts that we received all the DNs that we expected and
120         none are missing.
121         """
122
123         # Note that with GET_ANC Windows can end up sending the same parent
124         # object multiple times, so this might be noteworthy but doesn't
125         # warrant failing the test
126         if (len(received_list) != len(expected_list)):
127             print("Note: received %d objects but expected %d" %(len(received_list),
128                                                                 len(expected_list)))
129
130         # Check that we received every object that we were expecting
131         for dn in expected_list:
132             self.assertTrue(dn in received_list, "DN '%s' missing from replication." % dn)
133
134     def test_repl_integrity(self):
135         """
136         Modify the objects being replicated while the replication is still
137         in progress and check that no object loss occurs.
138         """
139
140         # The server behaviour differs between samba and Windows. Samba returns
141         # the objects in the original order (up to the pre-modify HWM). Windows
142         # incorporates the modified objects and returns them in the new order
143         # (i.e. modified objects last), up to the post-modify HWM. The Microsoft
144         # docs state the Windows behaviour is optional.
145
146         # Create a range of objects to replicate.
147         expected_dn_list = self.create_object_range(0, 400)
148         (orig_hwm, unused) = self._get_highest_hwm_utdv(self.ldb_dc1)
149
150         # We ask for the first page of 100 objects.
151         # For this test, we don't care what order we receive the objects in,
152         # so long as by the end we've received everything
153         rxd_dn_list = []
154         ctr6 = self.repl_get_next(rxd_dn_list)
155         rxd_dn_list = self._get_ctr6_dn_list(ctr6)
156
157         # Modify some of the second page of objects. This should bump the highwatermark
158         for x in range(100, 200):
159             self.modify_object(expected_dn_list[x], "displayName", "OU%d" % x)
160
161         (post_modify_hwm, unused) = self._get_highest_hwm_utdv(self.ldb_dc1)
162         self.assertTrue(post_modify_hwm.highest_usn > orig_hwm.highest_usn)
163
164         # Get the remaining blocks of data
165         while not self.replication_complete():
166             ctr6 = self.repl_get_next(rxd_dn_list)
167             rxd_dn_list += self._get_ctr6_dn_list(ctr6)
168
169         # Check we still receive all the objects we're expecting
170         self.assert_expected_data(rxd_dn_list, expected_dn_list)
171
172     def is_parent_known(self, dn, known_dn_list):
173         """
174         Returns True if the parent of the dn specified is in known_dn_list
175         """
176
177         # we can sometimes get system objects like the RID Manager returned.
178         # Ignore anything that is not under the test OU we created
179         if self.ou not in dn:
180             return True
181
182         # Remove the child portion from the name to get the parent's DN
183         name_substrings = dn.split(",")
184         del name_substrings[0]
185
186         parent_dn = ",".join(name_substrings)
187
188         # check either this object is a parent (it's parent is the top-level
189         # test object), or its parent has been seen previously
190         return parent_dn == self.ou or parent_dn in known_dn_list
191
192     def repl_get_next(self, initial_objects, get_anc=False):
193         """
194         Requests the next block of replication data. This tries to simulate
195         client behaviour - if we receive a replicated object that we don't know
196         the parent of, then re-request the block with the GET_ANC flag set.
197         """
198
199         # we're just trying to mimic regular client behaviour here, so just
200         # use the highwatermark in the last response we received
201         if self.last_ctr:
202             highwatermark = self.last_ctr.new_highwatermark
203             uptodateness_vector = self.last_ctr.uptodateness_vector
204         else:
205             # this is the initial replication, so we're starting from the start
206             highwatermark = None
207             uptodateness_vector = None
208
209         # we'll add new objects as we discover them, so take a copy to modify
210         known_objects = initial_objects[:]
211
212         # Ask for the next block of replication data
213         replica_flags = drsuapi.DRSUAPI_DRS_WRIT_REP
214
215         if get_anc:
216             replica_flags = drsuapi.DRSUAPI_DRS_WRIT_REP | drsuapi.DRSUAPI_DRS_GET_ANC
217             self.used_get_anc = True
218
219         ctr6 = self._get_replication(replica_flags,
220                                      max_objects=self.max_objects,
221                                      highwatermark=highwatermark,
222                                      uptodateness_vector=uptodateness_vector)
223
224         # check that we know the parent for every object received
225         rxd_dn_list = self._get_ctr6_dn_list(ctr6)
226
227         for i in range(0, len(rxd_dn_list)):
228
229             dn = rxd_dn_list[i]
230
231             if self.is_parent_known(dn, known_objects):
232
233                 # the new DN is now known so add it to the list.
234                 # It may be the parent of another child in this block
235                 known_objects.append(dn)
236             else:
237                 # If we've already set the GET_ANC flag then it should mean
238                 # we receive the parents before the child
239                 self.assertFalse(get_anc, "Unknown parent for object %s" % dn)
240
241                 print("Unknown parent for %s - try GET_ANC" % dn)
242
243                 # try the same thing again with the GET_ANC flag set this time
244                 return self.repl_get_next(get_anc=True)
245
246         # store the last successful result so we know what HWM to request next
247         self.last_ctr = ctr6
248
249         return ctr6
250
251     def replication_complete(self):
252         """Returns True if the current/last replication cycle is complete"""
253
254         if self.last_ctr is None or self.last_ctr.more_data:
255             return False
256         else:
257             return True
258
259     def test_repl_integrity_get_anc(self):
260         """
261         Modify the parent objects being replicated while the replication is still
262         in progress (using GET_ANC) and check that no object loss occurs.
263         """
264
265         # Note that GET_ANC behaviour varies between Windows and Samba.
266         # On Samba GET_ANC results in the replication restarting from the very
267         # beginning. After that, Samba remembers GET_ANC and also sends the
268         # parents in subsequent requests (regardless of whether GET_ANC is
269         # specified in the later request).
270         # Windows only sends the parents if GET_ANC was specified in the last
271         # request. It will also resend a parent, even if it's already sent the
272         # parent in a previous response (whereas Samba doesn't).
273
274         # Create a small block of 50 parents, each with 2 children (A and B)
275         # This is so that we receive some children in the first block, so we
276         # can resend with GET_ANC before we learn too many parents
277         parent_dn_list = []
278         expected_dn_list = self.create_object_range(0, 50, prefix="parent",
279                                                     children=("A", "B"),
280                                                     parent_list=parent_dn_list)
281
282         # create the remaining parents and children
283         expected_dn_list += self.create_object_range(50, 150, prefix="parent",
284                                                      children=("A", "B"),
285                                                      parent_list=parent_dn_list)
286
287         # We've now got objects in the following order:
288         # [50 parents][100 children][100 parents][200 children]
289
290         # Modify the first parent so that it's now ordered last by USN
291         # This means we set the GET_ANC flag pretty much straight away
292         # because we receive the first child before the first parent
293         self.modify_object(parent_dn_list[0], "displayName", "OU0")
294
295         # modify a later block of parents so they also get reordered
296         for x in range(50, 100):
297             self.modify_object(parent_dn_list[x], "displayName", "OU%d" % x)
298
299         # Get the first block of objects - this should resend the request with
300         # GET_ANC set because we won't know about the first child's parent.
301         # On samba GET_ANC essentially starts the sync from scratch again, so
302         # we get this over with early before we learn too many parents
303         rxd_dn_list = []
304         ctr6 = self.repl_get_next(rxd_dn_list)
305         rxd_dn_list = self._get_ctr6_dn_list(ctr6)
306
307         # modify the last chunk of parents. They should now have a USN higher
308         # than the highwater-mark for the replication cycle
309         for x in range(100, 150):
310             self.modify_object(parent_dn_list[x], "displayName", "OU%d" % x)
311
312         # Get the remaining blocks of data - this will resend the request with
313         # GET_ANC if it encounters an object it doesn't have the parent for.
314         while not self.replication_complete():
315             ctr6 = self.repl_get_next(rxd_dn_list)
316             rxd_dn_list += self._get_ctr6_dn_list(ctr6)
317
318         # The way the test objects have been created should force
319         # self.repl_get_next() to use the GET_ANC flag. If this doesn't
320         # actually happen, then the test isn't doing its job properly
321         self.assertTrue(self.used_get_anc,
322                         "Test didn't use the GET_ANC flag as expected")
323
324         # Check we get all the objects we're expecting
325         self.assert_expected_data(rxd_dn_list, expected_dn_list)
326