0636210d1e3001fc3406a193b2790f79dc2cefa2
[amitay/samba.git] / source4 / dsdb / tests / python / ad_dc_medley_performance.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 from __future__ import print_function
4
5 import optparse
6 import sys
7 sys.path.insert(0, 'bin/python')
8
9 import os
10 import samba
11 import samba.getopt as options
12 import random
13 import tempfile
14 import shutil
15 import time
16 import itertools
17
18 from samba.netcmd.main import cmd_sambatool
19
20 # We try to use the test infrastructure of Samba 4.3+, but if it
21 # doesn't work, we are probably in a back-ported patch and trying to
22 # run on 4.1 or something.
23 #
24 # Don't copy this horror into ordinary tests -- it is special for
25 # performance tests that want to apply to old versions.
26 try:
27     from samba.tests.subunitrun import SubunitOptions, TestProgram
28     ANCIENT_SAMBA = False
29 except ImportError:
30     ANCIENT_SAMBA = True
31     samba.ensure_external_module("testtools", "testtools")
32     samba.ensure_external_module("subunit", "subunit/python")
33     from subunit.run import SubunitTestRunner
34     import unittest
35
36 from samba.samdb import SamDB
37 from samba.auth import system_session
38 from ldb import Message, MessageElement, Dn, LdbError
39 from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
40 from ldb import SCOPE_BASE, SCOPE_SUBTREE, SCOPE_ONELEVEL
41 from ldb import ERR_NO_SUCH_OBJECT
42
43 parser = optparse.OptionParser("ad_dc_performance.py [options] <host>")
44 sambaopts = options.SambaOptions(parser)
45 parser.add_option_group(sambaopts)
46 parser.add_option_group(options.VersionOptions(parser))
47
48 if not ANCIENT_SAMBA:
49     subunitopts = SubunitOptions(parser)
50     parser.add_option_group(subunitopts)
51
52 # use command line creds if available
53 credopts = options.CredentialsOptions(parser)
54 parser.add_option_group(credopts)
55 opts, args = parser.parse_args()
56
57
58 if len(args) < 1:
59     parser.print_usage()
60     sys.exit(1)
61
62 host = args[0]
63
64 lp = sambaopts.get_loadparm()
65 creds = credopts.get_credentials(lp)
66
67 random.seed(1)
68
69
70 class PerfTestException(Exception):
71     pass
72
73
74 BATCH_SIZE = 2000
75 LINK_BATCH_SIZE = 1000
76 DELETE_BATCH_SIZE = 50
77 N_GROUPS = 29
78
79
80 class GlobalState(object):
81     next_user_id = 0
82     n_groups = 0
83     next_linked_user = 0
84     next_relinked_user = 0
85     next_linked_user_3 = 0
86     next_removed_link_0 = 0
87     test_number = 0
88     active_links = set()
89
90
91 class UserTests(samba.tests.TestCase):
92
93     def add_if_possible(self, *args, **kwargs):
94         """In these tests sometimes things are left in the database
95         deliberately, so we don't worry if we fail to add them a second
96         time."""
97         try:
98             self.ldb.add(*args, **kwargs)
99         except LdbError:
100             pass
101
102     def setUp(self):
103         super(UserTests, self).setUp()
104         self.state = GlobalState  # the class itself, not an instance
105         self.lp = lp
106         self.ldb = SamDB(host, credentials=creds,
107                          session_info=system_session(lp), lp=lp)
108         self.base_dn = self.ldb.domain_dn()
109         self.ou = "OU=pid%s,%s" % (os.getpid(), self.base_dn)
110         self.ou_users = "OU=users,%s" % self.ou
111         self.ou_groups = "OU=groups,%s" % self.ou
112         self.ou_computers = "OU=computers,%s" % self.ou
113
114         self.state.test_number += 1
115         random.seed(self.state.test_number)
116
117     def tearDown(self):
118         super(UserTests, self).tearDown()
119
120     def test_00_00_do_nothing(self):
121         # this gives us an idea of the overhead
122         pass
123
124     def test_00_01_do_nothing_relevant(self):
125         # takes around 1 second on i7-4770
126         j = 0
127         for i in range(30000000):
128             j += i
129
130     def test_00_02_do_nothing_sleepily(self):
131         time.sleep(1)
132
133     def test_00_03_add_ous_and_groups(self):
134         # initialise the database
135         for dn in (self.ou,
136                    self.ou_users,
137                    self.ou_groups,
138                    self.ou_computers):
139             self.ldb.add({
140                 "dn": dn,
141                 "objectclass": "organizationalUnit"
142             })
143
144         for i in range(N_GROUPS):
145             self.ldb.add({
146                 "dn": "cn=g%d,%s" % (i, self.ou_groups),
147                 "objectclass": "group"
148             })
149
150         self.state.n_groups = N_GROUPS
151
152     def _add_users(self, start, end):
153         for i in range(start, end):
154             self.ldb.add({
155                 "dn": "cn=u%d,%s" % (i, self.ou_users),
156                 "objectclass": "user"
157             })
158
159     def _add_users_ldif(self, start, end):
160         lines = []
161         for i in range(start, end):
162             lines.append("dn: cn=u%d,%s" % (i, self.ou_users))
163             lines.append("objectclass: user")
164             lines.append("")
165         self.ldb.add_ldif('\n'.join(lines))
166
167     def _test_join(self):
168         tmpdir = tempfile.mkdtemp()
169         if '://' in host:
170             server = host.split('://', 1)[1]
171         else:
172             server = host
173         cmd = cmd_sambatool.subcommands['domain'].subcommands['join']
174         result = cmd._run("samba-tool domain join",
175                           creds.get_realm(),
176                           "dc", "-U%s%%%s" % (creds.get_username(),
177                                               creds.get_password()),
178                           '--targetdir=%s' % tmpdir,
179                           '--server=%s' % server)
180
181         shutil.rmtree(tmpdir)
182
183     def _test_unindexed_search(self):
184         expressions = [
185             ('(&(objectclass=user)(description='
186              'Built-in account for adminstering the computer/domain))'),
187             '(description=Built-in account for adminstering the computer/domain)',
188             '(objectCategory=*)',
189             '(samaccountname=Administrator*)'
190         ]
191         for expression in expressions:
192             t = time.time()
193             for i in range(25):
194                 self.ldb.search(self.ou,
195                                 expression=expression,
196                                 scope=SCOPE_SUBTREE,
197                                 attrs=['cn'])
198             print('%d %s took %s' % (i, expression,
199                                      time.time() - t),
200                   file=sys.stderr)
201
202     def _test_indexed_search(self):
203         expressions = ['(objectclass=group)',
204                        '(samaccountname=Administrator)'
205                        ]
206         for expression in expressions:
207             t = time.time()
208             for i in range(4000):
209                 self.ldb.search(self.ou,
210                                 expression=expression,
211                                 scope=SCOPE_SUBTREE,
212                                 attrs=['cn'])
213             print('%d runs %s took %s' % (i, expression,
214                                           time.time() - t),
215                   file=sys.stderr)
216
217     def _test_base_search(self):
218         for dn in [self.base_dn, self.ou, self.ou_users,
219                    self.ou_groups, self.ou_computers]:
220             for i in range(4000):
221                 try:
222                     self.ldb.search(dn,
223                                     scope=SCOPE_BASE,
224                                     attrs=['cn'])
225                 except LdbError as e:
226                     (num, msg) = e.args
227                     if num != ERR_NO_SUCH_OBJECT:
228                         raise
229
230     def _test_base_search_failing(self):
231         pattern = 'missing%d' + self.ou
232         for i in range(4000):
233             try:
234                 self.ldb.search(pattern % i,
235                                 scope=SCOPE_BASE,
236                                 attrs=['cn'])
237             except LdbError as e:
238                 (num, msg) = e
239                 if num != ERR_NO_SUCH_OBJECT:
240                     raise
241
242     def search_expression_list(self, expressions, rounds,
243                                attrs=['cn'],
244                                scope=SCOPE_SUBTREE):
245         for expression in expressions:
246             t = time.time()
247             for i in range(rounds):
248                 self.ldb.search(self.ou,
249                                 expression=expression,
250                                 scope=SCOPE_SUBTREE,
251                                 attrs=['cn'])
252             print('%d runs %s took %s' % (i, expression,
253                                           time.time() - t),
254                   file=sys.stderr)
255
256     def _test_complex_search(self, n=100):
257         classes = ['samaccountname', 'objectCategory', 'dn', 'member']
258         values = ['*', '*t*', 'g*', 'user']
259         comparators = ['=', '<=', '>=']  # '~=' causes error
260         maybe_not = ['!(', '']
261         joiners = ['&', '|']
262
263         # The number of permuations is 18432, which is not huge but
264         # would take hours to search. So we take a sample.
265         all_permutations = list(itertools.product(joiners,
266                                                   classes, classes,
267                                                   values, values,
268                                                   comparators, comparators,
269                                                   maybe_not, maybe_not))
270
271         expressions = []
272
273         for (j, c1, c2, v1, v2,
274              o1, o2, n1, n2) in random.sample(all_permutations, n):
275             expression = ''.join(['(', j,
276                                   '(', n1, c1, o1, v1,
277                                   '))' if n1 else ')',
278                                   '(', n2, c2, o2, v2,
279                                   '))' if n2 else ')',
280                                   ')'])
281             expressions.append(expression)
282
283         self.search_expression_list(expressions, 1)
284
285     def _test_member_search(self, rounds=10):
286         expressions = []
287         for d in range(20):
288             expressions.append('(member=cn=u%d,%s)' % (d + 500, self.ou_users))
289             expressions.append('(member=u%d*)' % (d + 700,))
290
291         self.search_expression_list(expressions, rounds)
292
293     def _test_memberof_search(self, rounds=200):
294         expressions = []
295         for i in range(min(self.state.n_groups, rounds)):
296             expressions.append('(memberOf=cn=g%d,%s)' % (i, self.ou_groups))
297             expressions.append('(memberOf=cn=g%d*)' % (i,))
298             expressions.append('(memberOf=cn=*%s*)' % self.ou_groups)
299
300         self.search_expression_list(expressions, 2)
301
302     def _test_add_many_users(self, n=BATCH_SIZE):
303         s = self.state.next_user_id
304         e = s + n
305         self._add_users(s, e)
306         self.state.next_user_id = e
307
308     def _test_add_many_users_ldif(self, n=BATCH_SIZE):
309         s = self.state.next_user_id
310         e = s + n
311         self._add_users_ldif(s, e)
312         self.state.next_user_id = e
313
314     def _link_user_and_group(self, u, g):
315         link = (u, g)
316         if link in self.state.active_links:
317             return False
318
319         m = Message()
320         m.dn = Dn(self.ldb, "CN=g%d,%s" % (g, self.ou_groups))
321         m["member"] = MessageElement("cn=u%d,%s" % (u, self.ou_users),
322                                      FLAG_MOD_ADD, "member")
323         self.ldb.modify(m)
324         self.state.active_links.add(link)
325         return True
326
327     def _unlink_user_and_group(self, u, g):
328         link = (u, g)
329         if link not in self.state.active_links:
330             return False
331
332         user = "cn=u%d,%s" % (u, self.ou_users)
333         group = "CN=g%d,%s" % (g, self.ou_groups)
334         m = Message()
335         m.dn = Dn(self.ldb, group)
336         m["member"] = MessageElement(user, FLAG_MOD_DELETE, "member")
337         self.ldb.modify(m)
338         self.state.active_links.remove(link)
339         return True
340
341     def _test_link_many_users(self, n=LINK_BATCH_SIZE):
342         # this links unevenly, putting more users in the first group
343         # and fewer in the last.
344         ng = self.state.n_groups
345         nu = self.state.next_user_id
346         while n:
347             u = random.randrange(nu)
348             g = random.randrange(random.randrange(ng) + 1)
349             if self._link_user_and_group(u, g):
350                 n -= 1
351
352     def _test_link_many_users_batch(self, n=(LINK_BATCH_SIZE * 10)):
353         # this links unevenly, putting more users in the first group
354         # and fewer in the last.
355         ng = self.state.n_groups
356         nu = self.state.next_user_id
357         messages = []
358         for g in range(ng):
359             m = Message()
360             m.dn = Dn(self.ldb, "CN=g%d,%s" % (g, self.ou_groups))
361             messages.append(m)
362
363         while n:
364             u = random.randrange(nu)
365             g = random.randrange(random.randrange(ng) + 1)
366             link = (u, g)
367             if link in self.state.active_links:
368                 continue
369             m = messages[g]
370             m["member%s" % u] = MessageElement("cn=u%d,%s" %
371                                                (u, self.ou_users),
372                                                FLAG_MOD_ADD, "member")
373             self.state.active_links.add(link)
374             n -= 1
375
376         for m in messages:
377             try:
378                 self.ldb.modify(m)
379             except LdbError as e:
380                 print(e)
381                 print(m)
382
383     def _test_remove_some_links(self, n=(LINK_BATCH_SIZE // 2)):
384         victims = random.sample(list(self.state.active_links), n)
385         for x in victims:
386             self._unlink_user_and_group(*x)
387
388     test_00_11_join_empty_dc = _test_join
389
390     test_00_12_adding_users_2000 = _test_add_many_users
391
392     test_00_20_join_unlinked_2k_users = _test_join
393     test_00_21_unindexed_search_2k_users = _test_unindexed_search
394     test_00_22_indexed_search_2k_users = _test_indexed_search
395
396     test_00_23_complex_search_2k_users = _test_complex_search
397     test_00_24_member_search_2k_users = _test_member_search
398     test_00_25_memberof_search_2k_users = _test_memberof_search
399
400     test_00_27_base_search_2k_users = _test_base_search
401     test_00_28_base_search_failing_2k_users = _test_base_search_failing
402
403     test_01_01_link_2k_users = _test_link_many_users
404     test_01_02_link_2k_users_batch = _test_link_many_users_batch
405
406     test_02_10_join_2k_linked_dc = _test_join
407     test_02_11_unindexed_search_2k_linked_dc = _test_unindexed_search
408     test_02_12_indexed_search_2k_linked_dc = _test_indexed_search
409
410     test_04_01_remove_some_links_2k = _test_remove_some_links
411
412     test_05_01_adding_users_after_links_4k_ldif = _test_add_many_users_ldif
413
414     test_06_04_link_users_4k = _test_link_many_users
415     test_06_05_link_users_4k_batch = _test_link_many_users_batch
416
417     test_07_01_adding_users_after_links_6k = _test_add_many_users
418
419     def _test_ldif_well_linked_group(self, link_chance=1.0):
420         g = self.state.n_groups
421         self.state.n_groups += 1
422         lines = ["dn: CN=g%d,%s" % (g, self.ou_groups),
423                  "objectclass: group"]
424
425         for i in range(self.state.next_user_id):
426             if random.random() <= link_chance:
427                 lines.append("member: cn=u%d,%s" % (i, self.ou_users))
428                 self.state.active_links.add((i, g))
429
430         lines.append("")
431         self.ldb.add_ldif('\n'.join(lines))
432
433     test_09_01_add_fully_linked_group = _test_ldif_well_linked_group
434
435     def test_09_02_add_exponentially_diminishing_linked_groups(self):
436         linkage = 0.8
437         while linkage > 0.01:
438             self._test_ldif_well_linked_group(linkage)
439             linkage *= 0.75
440
441     test_09_04_link_users_6k = _test_link_many_users
442
443     test_10_01_unindexed_search_6k_users = _test_unindexed_search
444     test_10_02_indexed_search_6k_users = _test_indexed_search
445
446     test_10_27_base_search_6k_users = _test_base_search
447     test_10_28_base_search_failing_6k_users = _test_base_search_failing
448
449     def test_10_03_complex_search_6k_users(self):
450         self._test_complex_search(n=50)
451
452     def test_10_04_member_search_6k_users(self):
453         self._test_member_search(rounds=1)
454
455     def test_10_05_memberof_search_6k_users(self):
456         self._test_memberof_search(rounds=5)
457
458     test_11_02_join_full_dc = _test_join
459
460     test_12_01_remove_some_links_6k = _test_remove_some_links
461
462     def _test_delete_many_users(self, n=DELETE_BATCH_SIZE):
463         e = self.state.next_user_id
464         s = max(0, e - n)
465         self.state.next_user_id = s
466         for i in range(s, e):
467             self.ldb.delete("cn=u%d,%s" % (i, self.ou_users))
468
469         for x in tuple(self.state.active_links):
470             if s >= x[0] > e:
471                 self.state.active_links.remove(x)
472
473     test_20_01_delete_users_6k = _test_delete_many_users
474
475     def test_21_01_delete_10_groups(self):
476         for i in range(self.state.n_groups - 10, self.state.n_groups):
477             self.ldb.delete("cn=g%d,%s" % (i, self.ou_groups))
478         self.state.n_groups -= 10
479         for x in tuple(self.state.active_links):
480             if x[1] >= self.state.n_groups:
481                 self.state.active_links.remove(x)
482
483     test_21_02_delete_users_5950 = _test_delete_many_users
484
485     def test_22_01_delete_all_groups(self):
486         for i in range(self.state.n_groups):
487             self.ldb.delete("cn=g%d,%s" % (i, self.ou_groups))
488         self.state.n_groups = 0
489         self.state.active_links = set()
490
491     # XXX assert the state is as we think, using searches
492
493     def test_23_01_delete_users_5900_after_groups(self):
494         # we do not delete everything because it takes too long
495         n = 4 * DELETE_BATCH_SIZE
496         self._test_delete_many_users(n=n)
497
498     test_24_02_join_after_partial_cleanup = _test_join
499
500
501 if "://" not in host:
502     if os.path.isfile(host):
503         host = "tdb://%s" % host
504     else:
505         host = "ldap://%s" % host
506
507
508 if ANCIENT_SAMBA:
509     runner = SubunitTestRunner()
510     if not runner.run(unittest.makeSuite(UserTests)).wasSuccessful():
511         sys.exit(1)
512     sys.exit(0)
513 else:
514     TestProgram(module=__name__, opts=subunitopts)