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