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