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