getncchanges.py: Add test for GET_ANC and linked attributes
[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         self.rxd_dn_list = []
49         self.rxd_links = []
50
51         # 100 is the minimum max_objects that Microsoft seems to honour
52         # (the max honoured is 400ish), so we use that in these tests
53         self.max_objects = 100
54         self.last_ctr = None
55
56         # store whether we used GET_ANC flags in the requests
57         self.used_get_anc = False
58
59     def tearDown(self):
60         super(DrsReplicaSyncIntegrityTestCase, self).tearDown()
61         # tidyup groups and users
62         try:
63             self.ldb_dc1.delete(self.ou, ["tree_delete:1"])
64         except ldb.LdbError as (enum, string):
65             if enum == ldb.ERR_NO_SUCH_OBJECT:
66                 pass
67
68     def add_object(self, dn):
69         """Adds an OU object"""
70         self.ldb_dc1.add({"dn": dn, "objectclass": "organizationalunit"})
71         res = self.ldb_dc1.search(base=dn, scope=SCOPE_BASE)
72         self.assertEquals(len(res), 1)
73
74     def modify_object(self, dn, attr, value):
75         """Modifies an object's USN by adding an attribute value to it"""
76         m = ldb.Message()
77         m.dn = ldb.Dn(self.ldb_dc1, dn)
78         m[attr] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, attr)
79         self.ldb_dc1.modify(m)
80
81     def create_object_range(self, start, end, prefix="",
82                             children=None, parent_list=None):
83         """
84         Creates a block of objects. Object names are numbered sequentially,
85         using the optional prefix supplied. If the children parameter is
86         supplied it will create a parent-child hierarchy and return the
87         top-level parents separately.
88         """
89         dn_list = []
90
91         # Use dummy/empty lists if we're not creating a parent/child hierarchy
92         if children is None:
93             children = []
94
95         if parent_list is None:
96             parent_list = []
97
98         # Create the parents first, then the children.
99         # This makes it easier to see in debug when GET_ANC takes effect
100         # because the parent/children become interleaved (by default,
101         # this approach means the objects are organized into blocks of
102         # parents and blocks of children together)
103         for x in range(start, end):
104             ou = "OU=test_ou_%s%d,%s" % (prefix, x, self.ou)
105             self.add_object(ou)
106             dn_list.append(ou)
107
108             # keep track of the top-level parents (if needed)
109             parent_list.append(ou)
110
111         # create the block of children (if needed)
112         for x in range(start, end):
113             for child in children:
114                 ou = "OU=test_ou_child%s%d,%s" % (child, x, parent_list[x])
115                 self.add_object(ou)
116                 dn_list.append(ou)
117
118         return dn_list
119
120     def assert_expected_data(self, expected_list):
121         """
122         Asserts that we received all the DNs that we expected and
123         none are missing.
124         """
125         received_list = self.rxd_dn_list
126
127         # Note that with GET_ANC Windows can end up sending the same parent
128         # object multiple times, so this might be noteworthy but doesn't
129         # warrant failing the test
130         if (len(received_list) != len(expected_list)):
131             print("Note: received %d objects but expected %d" %(len(received_list),
132                                                                 len(expected_list)))
133
134         # Check that we received every object that we were expecting
135         for dn in expected_list:
136             self.assertTrue(dn in received_list, "DN '%s' missing from replication." % dn)
137
138     def test_repl_integrity(self):
139         """
140         Modify the objects being replicated while the replication is still
141         in progress and check that no object loss occurs.
142         """
143
144         # The server behaviour differs between samba and Windows. Samba returns
145         # the objects in the original order (up to the pre-modify HWM). Windows
146         # incorporates the modified objects and returns them in the new order
147         # (i.e. modified objects last), up to the post-modify HWM. The Microsoft
148         # docs state the Windows behaviour is optional.
149
150         # Create a range of objects to replicate.
151         expected_dn_list = self.create_object_range(0, 400)
152         (orig_hwm, unused) = self._get_highest_hwm_utdv(self.ldb_dc1)
153
154         # We ask for the first page of 100 objects.
155         # For this test, we don't care what order we receive the objects in,
156         # so long as by the end we've received everything
157         self.repl_get_next()
158
159         # Modify some of the second page of objects. This should bump the highwatermark
160         for x in range(100, 200):
161             self.modify_object(expected_dn_list[x], "displayName", "OU%d" % x)
162
163         (post_modify_hwm, unused) = self._get_highest_hwm_utdv(self.ldb_dc1)
164         self.assertTrue(post_modify_hwm.highest_usn > orig_hwm.highest_usn)
165
166         # Get the remaining blocks of data
167         while not self.replication_complete():
168             self.repl_get_next()
169
170         # Check we still receive all the objects we're expecting
171         self.assert_expected_data(expected_dn_list)
172
173     def is_parent_known(self, dn, known_dn_list):
174         """
175         Returns True if the parent of the dn specified is in known_dn_list
176         """
177
178         # we can sometimes get system objects like the RID Manager returned.
179         # Ignore anything that is not under the test OU we created
180         if self.ou not in dn:
181             return True
182
183         # Remove the child portion from the name to get the parent's DN
184         name_substrings = dn.split(",")
185         del name_substrings[0]
186
187         parent_dn = ",".join(name_substrings)
188
189         # check either this object is a parent (it's parent is the top-level
190         # test object), or its parent has been seen previously
191         return parent_dn == self.ou or parent_dn in known_dn_list
192
193     def _repl_send_request(self, get_anc=False):
194         """Sends a GetNCChanges request for the next block of replication data."""
195
196         # we're just trying to mimic regular client behaviour here, so just
197         # use the highwatermark in the last response we received
198         if self.last_ctr:
199             highwatermark = self.last_ctr.new_highwatermark
200             uptodateness_vector = self.last_ctr.uptodateness_vector
201         else:
202             # this is the first replication chunk
203             highwatermark = None
204             uptodateness_vector = None
205
206         # Ask for the next block of replication data
207         replica_flags = drsuapi.DRSUAPI_DRS_WRIT_REP
208
209         if get_anc:
210             replica_flags = drsuapi.DRSUAPI_DRS_WRIT_REP | drsuapi.DRSUAPI_DRS_GET_ANC
211             self.used_get_anc = True
212
213         # return the response from the DC
214         return self._get_replication(replica_flags,
215                                      max_objects=self.max_objects,
216                                      highwatermark=highwatermark,
217                                      uptodateness_vector=uptodateness_vector)
218
219     def repl_get_next(self, get_anc=False):
220         """
221         Requests the next block of replication data. This tries to simulate
222         client behaviour - if we receive a replicated object that we don't know
223         the parent of, then re-request the block with the GET_ANC flag set.
224         """
225
226         # send a request to the DC and get the response
227         ctr6 = self._repl_send_request(get_anc=get_anc)
228
229         # check that we know the parent for every object received
230         rxd_dn_list = self._get_ctr6_dn_list(ctr6)
231
232         # we'll add new objects as we discover them, so take a copy of the
233         # ones we already know about, so we can modify the list safely
234         known_objects = self.rxd_dn_list[:]
235
236         # check that we know the parent for every object received
237         for i in range(0, len(rxd_dn_list)):
238
239             dn = rxd_dn_list[i]
240
241             if self.is_parent_known(dn, known_objects):
242
243                 # the new DN is now known so add it to the list.
244                 # It may be the parent of another child in this block
245                 known_objects.append(dn)
246             else:
247                 # If we've already set the GET_ANC flag then it should mean
248                 # we receive the parents before the child
249                 self.assertFalse(get_anc, "Unknown parent for object %s" % dn)
250
251                 print("Unknown parent for %s - try GET_ANC" % dn)
252
253                 # try the same thing again with the GET_ANC flag set this time
254                 return self.repl_get_next(get_anc=True)
255
256         # store the last successful result so we know what HWM to request next
257         self.last_ctr = ctr6
258
259         # store the objects and links we received
260         self.rxd_dn_list += self._get_ctr6_dn_list(ctr6)
261         self.rxd_links += self._get_ctr6_links(ctr6)
262
263         return ctr6
264
265     def replication_complete(self):
266         """Returns True if the current/last replication cycle is complete"""
267
268         if self.last_ctr is None or self.last_ctr.more_data:
269             return False
270         else:
271             return True
272
273     def test_repl_integrity_get_anc(self):
274         """
275         Modify the parent objects being replicated while the replication is still
276         in progress (using GET_ANC) and check that no object loss occurs.
277         """
278
279         # Note that GET_ANC behaviour varies between Windows and Samba.
280         # On Samba GET_ANC results in the replication restarting from the very
281         # beginning. After that, Samba remembers GET_ANC and also sends the
282         # parents in subsequent requests (regardless of whether GET_ANC is
283         # specified in the later request).
284         # Windows only sends the parents if GET_ANC was specified in the last
285         # request. It will also resend a parent, even if it's already sent the
286         # parent in a previous response (whereas Samba doesn't).
287
288         # Create a small block of 50 parents, each with 2 children (A and B)
289         # This is so that we receive some children in the first block, so we
290         # can resend with GET_ANC before we learn too many parents
291         parent_dn_list = []
292         expected_dn_list = self.create_object_range(0, 50, prefix="parent",
293                                                     children=("A", "B"),
294                                                     parent_list=parent_dn_list)
295
296         # create the remaining parents and children
297         expected_dn_list += self.create_object_range(50, 150, prefix="parent",
298                                                      children=("A", "B"),
299                                                      parent_list=parent_dn_list)
300
301         # We've now got objects in the following order:
302         # [50 parents][100 children][100 parents][200 children]
303
304         # Modify the first parent so that it's now ordered last by USN
305         # This means we set the GET_ANC flag pretty much straight away
306         # because we receive the first child before the first parent
307         self.modify_object(parent_dn_list[0], "displayName", "OU0")
308
309         # modify a later block of parents so they also get reordered
310         for x in range(50, 100):
311             self.modify_object(parent_dn_list[x], "displayName", "OU%d" % x)
312
313         # Get the first block of objects - this should resend the request with
314         # GET_ANC set because we won't know about the first child's parent.
315         # On samba GET_ANC essentially starts the sync from scratch again, so
316         # we get this over with early before we learn too many parents
317         self.repl_get_next()
318
319         # modify the last chunk of parents. They should now have a USN higher
320         # than the highwater-mark for the replication cycle
321         for x in range(100, 150):
322             self.modify_object(parent_dn_list[x], "displayName", "OU%d" % x)
323
324         # Get the remaining blocks of data - this will resend the request with
325         # GET_ANC if it encounters an object it doesn't have the parent for.
326         while not self.replication_complete():
327             self.repl_get_next()
328
329         # The way the test objects have been created should force
330         # self.repl_get_next() to use the GET_ANC flag. If this doesn't
331         # actually happen, then the test isn't doing its job properly
332         self.assertTrue(self.used_get_anc,
333                         "Test didn't use the GET_ANC flag as expected")
334
335         # Check we get all the objects we're expecting
336         self.assert_expected_data(expected_dn_list)
337
338     def assert_expected_links(self, objects_with_links, link_attr="managedBy"):
339         """
340         Asserts that a GetNCChanges response contains any expected links
341         for the objects it contains.
342         """
343         received_links = self.rxd_links
344
345         num_expected = len(objects_with_links)
346
347         self.assertTrue(len(received_links) == num_expected,
348                         "Received %d links but expected %d"
349                         %(len(received_links), num_expected))
350
351         for dn in objects_with_links:
352             self.assert_object_has_link(dn, link_attr, received_links)
353
354     def assert_object_has_link(self, dn, link_attr, received_links):
355         """
356         Queries the object in the DB and asserts there is a link in the
357         GetNCChanges response that matches.
358         """
359
360         # Look up the link attribute in the DB
361         # The extended_dn option will dump the GUID info for the link
362         # attribute (as a hex blob)
363         res = self.ldb_dc1.search(ldb.Dn(self.ldb_dc1, dn), attrs=[link_attr],
364                                   controls=['extended_dn:1:0'], scope=ldb.SCOPE_BASE)
365
366         # We didn't find the expected link attribute in the DB for the object.
367         # Something has gone wrong somewhere...
368         self.assertTrue(link_attr in res[0], "%s in DB doesn't have attribute %s"
369                         %(dn, link_attr))
370
371         # find the received link in the list and assert that the target and
372         # source GUIDs match what's in the DB
373         for val in res[0][link_attr]:
374             # Work out the expected source and target GUIDs for the DB link
375             target_dn = ldb.Dn(self.ldb_dc1, val)
376             targetGUID_blob = target_dn.get_extended_component("GUID")
377             sourceGUID_blob = res[0].dn.get_extended_component("GUID")
378
379             found = False
380
381             for link in received_links:
382                 if link.selfGUID_blob == sourceGUID_blob and \
383                    link.targetGUID_blob == targetGUID_blob:
384
385                     found = True
386
387                     if self._debug:
388                         print("Link %s --> %s" %(dn[:25], link.targetDN[:25]))
389                     break
390
391             self.assertTrue(found, "Did not receive expected link for DN %s" % dn)
392
393     def test_repl_get_anc_link_attr(self):
394         """
395         A basic GET_ANC test where the parents have linked attributes
396         """
397
398         # Create a block of 100 parents and 100 children
399         parent_dn_list = []
400         expected_dn_list = self.create_object_range(0, 100, prefix="parent",
401                                                     children=("A"),
402                                                     parent_list=parent_dn_list)
403
404         # Add links from the parents to the children
405         for x in range(0, 100):
406             self.modify_object(parent_dn_list[x], "managedBy", expected_dn_list[x + 100])
407
408         # add some filler objects at the end. This allows us to easily see
409         # which chunk the links get sent in
410         expected_dn_list += self.create_object_range(0, 100, prefix="filler")
411
412         # We've now got objects in the following order:
413         # [100 x children][100 x parents][100 x filler]
414
415         # Get the replication data - because the block of children come first,
416         # this should retry the request with GET_ANC
417         while not self.replication_complete():
418             self.repl_get_next()
419
420         self.assertTrue(self.used_get_anc,
421                         "Test didn't use the GET_ANC flag as expected")
422
423         # Check we get all the objects we're expecting
424         self.assert_expected_data(expected_dn_list)
425
426         # Check we received links for all the parents
427         self.assert_expected_links(parent_dn_list)
428