2 # -*- coding: utf-8 -*-
3 # Originally based on ./sam.py
11 sys.path.insert(0, "bin/python")
13 from samba.tests.subunitrun import SubunitOptions, TestProgram
15 import samba.getopt as options
17 from samba.auth import system_session
19 from samba.samdb import SamDB
23 parser = optparse.OptionParser("vlv.py [options] <host>")
24 sambaopts = options.SambaOptions(parser)
25 parser.add_option_group(sambaopts)
26 parser.add_option_group(options.VersionOptions(parser))
27 # use command line creds if available
28 credopts = options.CredentialsOptions(parser)
29 parser.add_option_group(credopts)
30 subunitopts = SubunitOptions(parser)
31 parser.add_option_group(subunitopts)
33 parser.add_option('--elements', type='int', default=20,
34 help="use this many elements in the tests")
36 parser.add_option('--delete-in-setup', action='store_true',
37 help="cleanup in next setup rather than teardown")
39 parser.add_option('--skip-attr-regex',
40 help="ignore attributes matching this regex")
42 opts, args = parser.parse_args()
50 lp = sambaopts.get_loadparm()
51 creds = credopts.get_credentials(lp)
53 N_ELEMENTS = opts.elements
56 class VlvTestException(Exception):
60 def encode_vlv_control(critical=1,
66 s = "vlv:%d:%d:%d:" % (critical, before, after)
68 if offset is not None:
69 m = "%d:%d" % (offset, n)
70 elif ':' in gte or '\x00' in gte:
71 gte = base64.b64encode(gte)
72 m = "base64>=%s" % gte
79 return s + m + ':' + cookie
82 def get_cookie(controls, expected_n=None):
83 """Get the cookie, STILL base64 encoded, or raise ValueError."""
84 for c in list(controls):
86 if cstr.startswith('vlv_resp'):
87 head, n, _, cookie = cstr.rsplit(':', 3)
88 if expected_n is not None and int(n) != expected_n:
89 raise ValueError("Expected %s items, server said %s" %
92 raise ValueError("there is no VLV response")
95 class VLVTests(samba.tests.TestCase):
97 def create_user(self, i, n, prefix='vlvtest', suffix='', attrs=None):
98 name = "%s%d%s" % (prefix, i, suffix)
101 "objectclass": "user",
102 'givenName': "abcdefghijklmnopqrstuvwxyz"[i % 26],
103 "roomNumber": "%sbc" % (n - i),
105 "employeeNumber": "%s%sx" % (abs(i * (99 - i)), '\n' * (i & 255)),
106 "accountExpires": "%s" % (10 ** 9 + 1000000 * i),
107 "msTSExpireDate4": "19%02d0101010000.0Z" % (i % 100),
108 "flags": str(i * (n - i)),
109 "serialNumber": "abc %s%s%s" % ('AaBb |-/'[i & 7],
114 # _user_broken_attrs tests are broken due to problems outside
116 _user_broken_attrs = {
117 # Sort doesn't look past a NUL byte.
118 "photo": "\x00%d" % (n - i),
119 "audio": "%sn octet string %s%s ♫♬\x00lalala" % ('Aa'[i & 1],
121 "displayNamePrintable": "%d\x00%c" % (i, i & 255),
122 "adminDisplayName": "%d\x00b" % (n-i),
123 "title": "%d%sb" % (n - i, '\x00' * i),
124 "comment": "Favourite colour is %d" % (n % (i + 1)),
126 # Names that vary only in case. Windows returns
127 # equivalent addresses in the order they were put
128 # in ('a st', 'A st',...).
129 "street": "%s st" % (chr(65 | (i & 14) | ((i & 1) * 32))),
132 if attrs is not None:
135 user['dn'] = "cn=%s,%s" % (user['cn'], self.ou)
137 if opts.skip_attr_regex:
138 match = re.compile(opts.skip_attr_regex).search
139 for k in user.keys():
143 self.users.append(user)
148 super(VLVTests, self).setUp()
149 self.ldb = SamDB(host, credentials=creds,
150 session_info=system_session(lp), lp=lp)
152 self.base_dn = self.ldb.domain_dn()
153 self.ou = "ou=vlv,%s" % self.base_dn
154 if opts.delete_in_setup:
156 self.ldb.delete(self.ou, ['tree_delete:1'])
157 except ldb.LdbError, e:
158 print "tried deleting %s, got error %s" % (self.ou, e)
161 "objectclass": "organizationalUnit"})
164 for i in range(N_ELEMENTS):
165 self.create_user(i, N_ELEMENTS)
167 attrs = self.users[0].keys()
168 self.binary_sorted_keys = ['audio',
172 "displayNamePrintable"]
174 self.numeric_sorted_keys = ['flags',
177 self.timestamp_keys = ['msTSExpireDate4']
179 self.int64_keys = set(['accountExpires'])
181 self.locale_sorted_keys = [x for x in attrs if
182 x not in (self.binary_sorted_keys +
183 self.numeric_sorted_keys)]
185 # don't try spaces, etc in cn
186 self.delicate_keys = ['cn']
189 super(VLVTests, self).tearDown()
190 if not opts.delete_in_setup:
191 self.ldb.delete(self.ou, ['tree_delete:1'])
193 def get_full_list(self, attr, include_cn=False):
194 """Fetch the whole list sorted on the attribute, using the VLV.
195 This way you get a VLV cookie."""
196 n_users = len(self.users)
197 sort_control = "server_sort:1:0:%s" % attr
198 half_n = n_users // 2
199 vlv_search = "vlv:1:%d:%d:%d:0" % (half_n, half_n, half_n + 1)
203 res = self.ldb.search(self.ou,
204 scope=ldb.SCOPE_ONELEVEL,
206 controls=[sort_control,
209 full_results = [(x[attr][0], x['cn'][0]) for x in res]
211 full_results = [x[attr][0].lower() for x in res]
212 controls = res.controls
213 return full_results, controls, sort_control
215 def get_expected_order(self, attr, expression=None):
216 """Fetch the whole list sorted on the attribute, using sort only."""
217 sort_control = "server_sort:1:0:%s" % attr
218 res = self.ldb.search(self.ou,
219 scope=ldb.SCOPE_ONELEVEL,
220 expression=expression,
222 controls=[sort_control])
223 results = [x[attr][0] for x in res]
226 def delete_user(self, user):
227 self.ldb.delete(user['dn'])
228 del self.users[self.users.index(user)]
230 def get_gte_tests_and_order(self, attr, expression=None):
231 expected_order = self.get_expected_order(attr, expression=expression)
233 if attr in self.delicate_keys:
241 elif attr in self.timestamp_keys:
252 elif attr not in self.numeric_sorted_keys:
266 gte_keys.append(expected_order[len(expected_order) // 2] + ' tail')
269 # "numeric" means positive integers
270 # doesn't work with -1, 3.14, ' 3', '9' * 20
277 if attr in self.int64_keys:
278 gte_keys += ['3' * 12, '71' * 8]
280 for i, x in enumerate(gte_keys):
281 user = self.create_user(i, N_ELEMENTS,
284 gte_users.append(user)
286 gte_order = self.get_expected_order(attr)
287 for user in gte_users:
288 self.delete_user(user)
291 expected_order_2 = self.get_expected_order(attr, expression=expression)
292 self.assertEqual(expected_order, expected_order_2)
294 # Map gte tests to indexes in expected order. This will break
295 # if gte_order and expected_order are differently ordered (as
299 # index to the first one with each value
301 for i, k in enumerate(expected_order):
302 if k not in index_map:
317 gte_map[k] = len(expected_order)
322 print " %10s => %10s" % (k, gte_map[k])
324 return gte_order, expected_order, gte_map
326 def assertCorrectResults(self, results, expected_order,
327 offset, before, after):
328 """A helper to calculate offsets correctly and say as much as possible
329 when something goes wrong."""
331 start = max(offset - before - 1, 0)
333 expected_results = expected_order[start: end]
335 # if it is a tuple with the cn, drop the cn
336 if expected_results and isinstance(expected_results[0], tuple):
337 expected_results = [x[0] for x in expected_results]
339 if expected_results == results:
342 if expected_order is not None:
343 print "expected order: %s" % expected_order
345 print "offset %d before %d after %d" % (offset, before, after)
346 print "start %d end %d" % (start, end)
347 print "expected: %s" % expected_results
348 print "got : %s" % results
349 self.assertEquals(expected_results, results)
351 def test_server_vlv_with_cookie(self):
352 attrs = [x for x in self.users[0].keys() if x not in
353 ('dn', 'objectclass')]
355 expected_order = self.get_expected_order(attr)
356 sort_control = "server_sort:1:0:%s" % attr
359 for before in range(0, 9):
360 for after in range(0, 9):
361 for offset in range(max(1, before - 2),
362 min(n - after + 2, n)):
364 vlv_search = "vlv:1:%d:%d:%d:0" % (before, after,
367 cookie = get_cookie(res.controls, n)
368 vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
369 (before, after, offset, n,
372 res = self.ldb.search(self.ou,
373 scope=ldb.SCOPE_ONELEVEL,
375 controls=[sort_control,
378 results = [x[attr][0] for x in res]
380 self.assertCorrectResults(results, expected_order,
381 offset, before, after)
383 def run_index_tests_with_expressions(self, expressions):
384 # Here we don't test every before/after combination.
385 attrs = [x for x in self.users[0].keys() if x not in
386 ('dn', 'objectclass')]
388 for expression in expressions:
389 expected_order = self.get_expected_order(attr, expression)
390 sort_control = "server_sort:1:0:%s" % attr
392 n = len(expected_order)
393 for before in range(0, 11):
395 for offset in range(max(1, before - 2),
396 min(n - after + 2, n)):
398 vlv_search = "vlv:1:%d:%d:%d:0" % (before, after,
401 cookie = get_cookie(res.controls)
402 vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
403 (before, after, offset, n,
406 res = self.ldb.search(self.ou,
407 expression=expression,
408 scope=ldb.SCOPE_ONELEVEL,
410 controls=[sort_control,
413 results = [x[attr][0] for x in res]
415 self.assertCorrectResults(results, expected_order,
416 offset, before, after)
418 def test_server_vlv_with_expression(self):
419 """What happens when we run the VLV with an expression?"""
420 expressions = ["(objectClass=*)",
421 "(cn=%s)" % self.users[-1]['cn'],
422 "(roomNumber=%s)" % self.users[0]['roomNumber'],
424 self.run_index_tests_with_expressions(expressions)
426 def test_server_vlv_with_failing_expression(self):
427 """What happens when we run the VLV on an expression that matches
429 expressions = ["(samaccountname=testferf)",
432 self.run_index_tests_with_expressions(expressions)
434 def run_gte_tests_with_expressions(self, expressions):
435 # Here we don't test every before/after combination.
436 attrs = [x for x in self.users[0].keys() if x not in
437 ('dn', 'objectclass')]
438 for expression in expressions:
440 gte_order, expected_order, gte_map = \
441 self.get_gte_tests_and_order(attr, expression)
442 # In case there is some order dependency, disorder tests
443 gte_tests = gte_order[:]
445 random.shuffle(gte_tests)
447 sort_control = "server_sort:1:0:%s" % attr
449 expected_order = self.get_expected_order(attr, expression)
450 sort_control = "server_sort:1:0:%s" % attr
452 for before in range(0, 11):
454 for gte in gte_tests:
456 cookie = get_cookie(res.controls)
459 vlv_search = encode_vlv_control(before=before,
464 res = self.ldb.search(self.ou,
465 scope=ldb.SCOPE_ONELEVEL,
466 expression=expression,
468 controls=[sort_control,
471 results = [x[attr][0] for x in res]
472 offset = gte_map.get(gte, len(expected_order))
474 # here offset is 0-based
475 start = max(offset - before, 0)
476 end = offset + 1 + after
478 expected_results = expected_order[start: end]
480 self.assertEquals(expected_results, results)
482 def test_vlv_gte_with_expression(self):
483 """What happens when we run the VLV with an expression?"""
484 expressions = ["(objectClass=*)",
485 "(cn=%s)" % self.users[-1]['cn'],
486 "(roomNumber=%s)" % self.users[0]['roomNumber'],
488 self.run_gte_tests_with_expressions(expressions)
490 def test_vlv_gte_with_failing_expression(self):
491 """What happens when we run the VLV on an expression that matches
493 expressions = ["(samaccountname=testferf)",
496 self.run_gte_tests_with_expressions(expressions)
498 def test_server_vlv_with_cookie_while_adding_and_deleting(self):
499 """What happens if we add or remove items in the middle of the VLV?
501 Nothing. The search and the sort is not repeated, and we only
502 deal with ther objects originally found.
504 attrs = ['cn'] + [x for x in self.users[0].keys() if x not in
505 ('dn', 'objectclass')]
509 full_results, controls, sort_control = \
510 self.get_full_list(attr, True)
511 original_n = len(self.users)
513 expected_order = full_results
516 for before in range(0, 3) + [6, 11, 19]:
517 for after in range(0, 3) + [6, 11, 19]:
518 start = max(before - 1, 1)
519 end = max(start + 4, original_n - after + 2)
520 for offset in range(start, end):
521 #if iteration > 2076:
523 cookie = get_cookie(controls, original_n)
524 vlv_search = encode_vlv_control(before=before,
531 res = self.ldb.search(self.ou,
532 scope=ldb.SCOPE_ONELEVEL,
534 controls=[sort_control,
537 controls = res.controls
538 results = [x[attr][0] for x in res]
539 real_offset = max(1, min(offset, len(expected_order)))
541 expected_results = []
543 begin_offset = max(real_offset - before - 1, 0)
544 real_before = min(before, real_offset - 1)
545 real_after = min(after,
546 len(expected_order) - real_offset)
548 for x in expected_order[begin_offset:]:
550 expected_results.append(x[0])
551 if (len(expected_results) ==
552 real_before + real_after + 1):
557 if expected_results != results:
558 print ("attr %s before %d after %d offset %d" %
559 (attr, before, after, offset))
560 self.assertEquals(expected_results, results)
563 if random.random() < 0.1 + (n < 5) * 0.05:
567 i = random.randrange(n)
568 user = self.create_user(i, n, suffix='-%s' %
571 if random.random() < 0.1 + (n > 50) * 0.02 and n:
572 index = random.randrange(n)
573 user = self.users.pop(index)
575 self.ldb.delete(user['dn'])
577 replaced = (user[attr], user['cn'])
578 if replaced in expected_order:
579 i = expected_order.index(replaced)
580 expected_order[i] = None
582 def test_server_vlv_with_cookie_while_changing(self):
583 """What happens if we modify items in the middle of the VLV?
585 The expected behaviour (as found on Windows) is the sort is
586 not repeated, but the changes in attributes are reflected.
588 attrs = [x for x in self.users[0].keys() if x not in
589 ('dn', 'objectclass', 'cn')]
591 n_users = len(self.users)
592 expected_order = [x.upper() for x in self.get_expected_order(attr)]
593 sort_control = "server_sort:1:0:%s" % attr
597 # First we'll fetch the whole list so we know the original
598 # sort order. This is necessary because we don't know how
599 # the server will order equivalent items. We are using the
601 half_n = n_users // 2
602 vlv_search = "vlv:1:%d:%d:%d:0" % (half_n, half_n, half_n + 1)
603 res = self.ldb.search(self.ou,
604 scope=ldb.SCOPE_ONELEVEL,
606 controls=[sort_control, vlv_search])
608 results = [x[attr][0].upper() for x in res]
609 #self.assertEquals(expected_order, results)
611 dn_order = [str(x['dn']) for x in res]
614 for before in range(0, 3):
615 for after in range(0, 3):
616 for offset in range(1 + before, n_users - after):
617 cookie = get_cookie(res.controls, len(self.users))
618 vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
619 (before, after, offset, len(self.users),
622 res = self.ldb.search(self.ou,
623 scope=ldb.SCOPE_ONELEVEL,
625 controls=[sort_control,
628 dn_results = [str(x['dn']) for x in res]
629 dn_expected = dn_order[offset - before - 1:
632 self.assertEquals(dn_expected, dn_results)
634 results = [x[attr][0].upper() for x in res]
636 self.assertCorrectResults(results, values,
637 offset, before, after)
641 if (attr in self.locale_sorted_keys or
642 attr in self.binary_sorted_keys):
644 i2 = (i ^ 255) % n_users
649 if v2 in self.locale_sorted_keys:
651 cn1 = dn1.split(',', 1)[0][3:]
652 cn2 = dn2.split(',', 1)[0][3:]
657 m.dn = ldb.Dn(self.ldb, dn1)
658 m[attr] = ldb.MessageElement(v2,
659 ldb.FLAG_MOD_REPLACE,
664 def test_server_vlv_fractions_with_cookie(self):
665 """What happens when the count is set to an arbitrary number?
667 In that case the offset and the count form a fraction, and the
668 VLV should be centred at a point offset/count of the way
669 through. For example, if offset is 3 and count is 6, the VLV
670 should be looking around halfway. The actual algorithm is a
671 bit fiddlier than that, because of the one-basedness of VLV.
673 attrs = [x for x in self.users[0].keys() if x not in
674 ('dn', 'objectclass')]
676 n_users = len(self.users)
681 full_results, controls, sort_control = self.get_full_list(attr)
682 self.assertEqual(len(full_results), n_users)
683 for before in range(0, 2):
684 for after in range(0, 2):
685 for denominator in range(1, 20):
686 for offset in range(1, denominator + 3):
687 cookie = get_cookie(controls, len(self.users))
688 vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
689 (before, after, offset,
693 res = self.ldb.search(self.ou,
694 scope=ldb.SCOPE_ONELEVEL,
696 controls=[sort_control,
698 except ldb.LdbError, e:
701 print ("offset %d denominator %d raised error "
702 "expected error %s\n"
703 "(offset zero is illegal unless "
704 "content count is zero)" %
705 (offset, denominator, e))
708 results = [x[attr][0].lower() for x in res]
711 denominator = n_users
714 elif denominator == 1:
715 # the offset can only be 1, but the 1/1 case
716 # means something special
718 real_offset = n_users
722 if offset > denominator:
725 int(round((n_users - 1) *
727 (denominator - 1.0)))
730 self.assertCorrectResults(results, full_results,
734 controls = res.controls
736 for c in list(controls):
738 if cstr.startswith('vlv_resp'):
739 bits = cstr.rsplit(':')
740 print ("the answer is %s; we said %d" %
741 (bits[2], real_offset))
744 def test_server_vlv_no_cookie(self):
745 attrs = [x for x in self.users[0].keys() if x not in
746 ('dn', 'objectclass')]
749 expected_order = self.get_expected_order(attr)
750 sort_control = "server_sort:1:0:%s" % attr
751 for before in range(0, 5):
752 for after in range(0, 7):
753 for offset in range(1 + before, len(self.users) - after):
754 res = self.ldb.search(self.ou,
755 scope=ldb.SCOPE_ONELEVEL,
757 controls=[sort_control,
761 results = [x[attr][0] for x in res]
762 self.assertCorrectResults(results, expected_order,
763 offset, before, after)
765 def test_server_vlv_gte_with_cookie(self):
766 attrs = [x for x in self.users[0].keys() if x not in
767 ('dn', 'objectclass')]
769 gte_order, expected_order, gte_map = \
770 self.get_gte_tests_and_order(attr)
771 # In case there is some order dependency, disorder tests
772 gte_tests = gte_order[:]
774 random.shuffle(gte_tests)
776 sort_control = "server_sort:1:0:%s" % attr
777 for before in range(0, 5):
778 for after in range(0, 7):
779 for gte in gte_tests:
781 cookie = get_cookie(res.controls, len(self.users))
784 vlv_search = encode_vlv_control(before=before,
789 res = self.ldb.search(self.ou,
790 scope=ldb.SCOPE_ONELEVEL,
792 controls=[sort_control,
795 results = [x[attr][0] for x in res]
796 offset = gte_map.get(gte, len(expected_order))
798 # here offset is 0-based
799 start = max(offset - before, 0)
800 end = offset + 1 + after
802 expected_results = expected_order[start: end]
804 self.assertEquals(expected_results, results)
806 def test_server_vlv_gte_no_cookie(self):
807 attrs = [x for x in self.users[0].keys() if x not in
808 ('dn', 'objectclass')]
811 gte_order, expected_order, gte_map = \
812 self.get_gte_tests_and_order(attr)
813 # In case there is some order dependency, disorder tests
814 gte_tests = gte_order[:]
816 random.shuffle(gte_tests)
818 sort_control = "server_sort:1:0:%s" % attr
819 for before in range(0, 4):
820 for after in range(0, 5):
821 for gte in gte_tests:
822 if attr == 'audio' and 0:
825 vlv_search = encode_vlv_control(before=before,
829 res = self.ldb.search(self.ou,
830 scope=ldb.SCOPE_ONELEVEL,
832 controls=[sort_control,
834 results = [x[attr][0] for x in res]
836 # here offset is 0-based
837 offset = gte_map.get(gte, len(expected_order))
838 start = max(offset - before, 0)
839 end = offset + after + 1
840 expected_results = expected_order[start: end]
842 if expected_results != results:
843 middle = expected_order[len(expected_order) // 2]
844 print expected_results, results
848 print ("\nattr %s offset %d before %d "
850 (attr, offset, before, after, gte))
851 self.assertEquals(expected_results, results)
853 def test_multiple_searches(self):
854 """The maximum number of concurrent vlv searches per connection is
855 currently set at 3. That means if you open 4 VLV searches the
856 cookie on the first one should fail.
858 # Windows has a limit of 10 VLVs where there are low numbers
859 # of objects in each search.
860 attrs = ([x for x in self.users[0].keys() if x not in
861 ('dn', 'objectclass')] * 2)[:12]
865 sort_control = "server_sort:1:0:%s" % attr
867 res = self.ldb.search(self.ou,
868 scope=ldb.SCOPE_ONELEVEL,
870 controls=[sort_control,
873 cookie = get_cookie(res.controls, len(self.users))
874 vlv_cookies.append(cookie)
877 # now this one should fail
878 self.assertRaises(ldb.LdbError,
881 scope=ldb.SCOPE_ONELEVEL,
883 controls=[sort_control,
884 "vlv:1:1:1:1:0:%s" % vlv_cookies[0]])
886 # and this one should succeed
887 res = self.ldb.search(self.ou,
888 scope=ldb.SCOPE_ONELEVEL,
890 controls=[sort_control,
891 "vlv:1:1:1:1:0:%s" % vlv_cookies[-1]])
893 # this one should fail because it is a new connection and
894 # doesn't share cookies
895 new_ldb = SamDB(host, credentials=creds,
896 session_info=system_session(lp), lp=lp)
898 self.assertRaises(ldb.LdbError,
899 new_ldb.search, self.ou,
900 scope=ldb.SCOPE_ONELEVEL,
902 controls=[sort_control,
903 "vlv:1:1:1:1:0:%s" % vlv_cookies[-1]])
905 # but now without the critical flag it just does no VLV.
906 new_ldb.search(self.ou,
907 scope=ldb.SCOPE_ONELEVEL,
909 controls=[sort_control,
910 "vlv:0:1:1:1:0:%s" % vlv_cookies[-1]])
913 if "://" not in host:
914 if os.path.isfile(host):
915 host = "tdb://%s" % host
917 host = "ldap://%s" % host
920 TestProgram(module=__name__, opts=subunitopts)