|
13 | 13 |
|
14 | 14 | import ldap
|
15 | 15 | from ldap.ldapobject import SimpleLDAPObject
|
16 |
| -from ldap.syncrepl import SyncreplConsumer, SyncInfoMessage |
| 16 | +from ldap.syncrepl import SyncreplConsumer, SyncInfoMessage, \ |
| 17 | + OpenLDAPSyncreplCookie |
17 | 18 |
|
18 | 19 | from slapdtest import SlapdObject, SlapdTestCase
|
19 | 20 |
|
|
37 | 38 | olcModuleLoad: back_%(database)s
|
38 | 39 | olcModuleLoad: syncprov
|
39 | 40 |
|
| 41 | +dn: olcDatabase=config,cn=config |
| 42 | +objectClass: olcDatabaseConfig |
| 43 | +olcRootDN: %(rootdn)s |
| 44 | +
|
40 | 45 | dn: olcDatabase=%(database)s,cn=config
|
41 | 46 | objectClass: olcDatabaseConfig
|
42 | 47 | objectClass: olcMdbConfig
|
@@ -442,6 +447,174 @@ def setUp(self):
|
442 | 447 | self.suffix = self.server.suffix
|
443 | 448 |
|
444 | 449 |
|
| 450 | +class TestMPRSyncrepl(BaseSyncreplTests, SlapdTestCase): |
| 451 | + class MPRClient(SyncreplClient): |
| 452 | + def __init__(self, *args, **kwargs): |
| 453 | + super().__init__(*args, **kwargs) |
| 454 | + self.cookie = OpenLDAPSyncreplCookie() |
| 455 | + |
| 456 | + def syncrepl_set_cookie(self, cookie): |
| 457 | + self.cookie.update(cookie) |
| 458 | + super().syncrepl_set_cookie(self.cookie.unparse()) |
| 459 | + |
| 460 | + def setUp(self): |
| 461 | + super().setUp() |
| 462 | + self.tester = self.MPRClient( |
| 463 | + self.server.ldap_uri, |
| 464 | + self.server.root_dn, |
| 465 | + self.server.root_pw, |
| 466 | + bytes_mode=False |
| 467 | + ) |
| 468 | + self.suffix = self.server.suffix |
| 469 | + |
| 470 | + # An active MPR should not have a sid=000 server in it |
| 471 | + if self.server.server_id == 0: |
| 472 | + self.skip("Server got serverid 0 assigned") |
| 473 | + |
| 474 | + def test_mpr_refresh_and_persist(self): |
| 475 | + """ |
| 476 | + Make sure we process cookie updates from a live MPR cluster correctly |
| 477 | + """ |
| 478 | + # Assumes that server_id is not used before the call to start() |
| 479 | + self.server2 = self.server_class() |
| 480 | + if self.server.server_id == self.server2.server_id: |
| 481 | + self.server2.server_id += 1 |
| 482 | + if self.server2.server_id % 4096 == 0: |
| 483 | + self.server2.server_id = 1 |
| 484 | + |
| 485 | + with self.server2 as server2: |
| 486 | + tester2 = self.MPRClient( |
| 487 | + self.server2.ldap_uri, |
| 488 | + self.server2.root_dn, |
| 489 | + self.server2.root_pw, |
| 490 | + bytes_mode=False |
| 491 | + ) |
| 492 | + self.addCleanup(tester2.unbind_s) |
| 493 | + |
| 494 | + self.tester.search( |
| 495 | + self.suffix, |
| 496 | + 'refreshAndPersist', |
| 497 | + ) |
| 498 | + |
| 499 | + # Run a quick refresh, that shouldn't have any changes. |
| 500 | + while self.tester.refresh_done is not True: |
| 501 | + poll_result = self.tester.poll( |
| 502 | + all=0, |
| 503 | + timeout=None |
| 504 | + ) |
| 505 | + self.assertTrue(poll_result) |
| 506 | + |
| 507 | + # Again, server data should not have changed. |
| 508 | + self.assertEqual(self.tester.dn_attrs, LDAP_ENTRIES) |
| 509 | + |
| 510 | + # set up replication between both |
| 511 | + coords = [(1, self.server.ldap_uri, self.suffix, |
| 512 | + self.server.root_dn, self.server.root_pw), |
| 513 | + (2, self.server2.ldap_uri, self.suffix, |
| 514 | + self.server2.root_dn, self.server2.root_pw), |
| 515 | + ] |
| 516 | + modifications = [ |
| 517 | + (ldap.MOD_ADD, "olcSyncrepl", [ |
| 518 | + ('rid=%d provider=%s searchbase="%s" type=refreshAndPersist ' |
| 519 | + 'bindmethod=simple binddn="%s" credentials="%s" ' |
| 520 | + 'retry="1 +"' % coord).encode() for coord in coords]), |
| 521 | + # do we still support 2.4.x? Change to olcMultiProvider if not |
| 522 | + (ldap.MOD_REPLACE, "olcMirrorMode", [b"TRUE"]), |
| 523 | + ] |
| 524 | + |
| 525 | + self.tester.modify_s( |
| 526 | + "olcDatabase={1}%s,cn=config" % (self.server.database), |
| 527 | + modifications) |
| 528 | + tester2.modify_s( |
| 529 | + "olcDatabase={1}%s,cn=config" % (self.server.database), |
| 530 | + modifications) |
| 531 | + |
| 532 | + tester2.search( |
| 533 | + self.suffix, |
| 534 | + 'refreshAndPersist', |
| 535 | + ) |
| 536 | + |
| 537 | + # Wait till server2 catches up |
| 538 | + while tester2.refresh_done is not True or \ |
| 539 | + tester2.cookie.unparse() != self.tester.cookie.unparse(): |
| 540 | + try: |
| 541 | + poll_result = tester2.poll( |
| 542 | + all=0, |
| 543 | + timeout=None |
| 544 | + ) |
| 545 | + self.assertTrue(poll_result) |
| 546 | + except ldap.NO_SUCH_OBJECT: |
| 547 | + # 2.6+ Allows a refreshAndPersist against an empty DB, but |
| 548 | + # with older ones we need to retry until there's at least |
| 549 | + # one entry |
| 550 | + tester2.search( |
| 551 | + self.suffix, |
| 552 | + 'refreshAndPersist', |
| 553 | + ) |
| 554 | + |
| 555 | + # Again, server data should not have changed. |
| 556 | + self.assertEqual(tester2.dn_attrs, LDAP_ENTRIES) |
| 557 | + |
| 558 | + # From here on, things get little hairy, server1 might not have |
| 559 | + # finished its refresh from 2 and we can't easily confirm this |
| 560 | + # without cn=monitor. We just read back our CSNs and make sure |
| 561 | + # we've seen both. |
| 562 | + |
| 563 | + # send some mods to both |
| 564 | + modification = [('objectClass', [b'device'])] |
| 565 | + self.tester.add_s("cn=server1,%s" % self.suffix, modification) |
| 566 | + |
| 567 | + csn1 = self.tester.read_s("cn=server1,%s" % self.suffix, |
| 568 | + attrlist=['entryCSN'] |
| 569 | + )['entryCSN'][0].decode('utf8') |
| 570 | + |
| 571 | + tester2.add_s("cn=server2,%s" % self.suffix, modification) |
| 572 | + csn2 = tester2.read_s("cn=server2,%s" % self.suffix, |
| 573 | + attrlist=['entryCSN'] |
| 574 | + )['entryCSN'][0].decode('utf8') |
| 575 | + |
| 576 | + new_state = LDAP_ENTRIES.copy() |
| 577 | + new_state["cn=server1,%s" % self.suffix] = { |
| 578 | + "objectClass": [b"device"], |
| 579 | + "cn": [b"server1"], |
| 580 | + } |
| 581 | + new_state["cn=server2,%s" % self.suffix] = { |
| 582 | + "objectClass": [b"device"], |
| 583 | + "cn": [b"server2"], |
| 584 | + } |
| 585 | + |
| 586 | + # Wait for the cookie to sync up, a failure would be that this |
| 587 | + # doesn't happen, so impose a timeout |
| 588 | + while csn1 not in self.tester.cookie.unparse() or \ |
| 589 | + csn2 not in self.tester.cookie.unparse() or \ |
| 590 | + csn1 not in tester2.cookie.unparse() or \ |
| 591 | + csn2 not in tester2.cookie.unparse(): |
| 592 | + if csn1 not in self.tester.cookie.unparse() or \ |
| 593 | + csn2 not in self.tester.cookie.unparse(): |
| 594 | + poll_result = self.tester.poll( |
| 595 | + all=0, |
| 596 | + timeout=5 |
| 597 | + ) |
| 598 | + self.assertTrue(poll_result) |
| 599 | + if csn1 not in tester2.cookie.unparse() or \ |
| 600 | + csn2 not in tester2.cookie.unparse(): |
| 601 | + poll_result = tester2.poll( |
| 602 | + all=0, |
| 603 | + timeout=5 |
| 604 | + ) |
| 605 | + self.assertTrue(poll_result) |
| 606 | + |
| 607 | + self.assertEqual(self.tester.cookie.unparse(), |
| 608 | + tester2.cookie.unparse()) |
| 609 | + self.assertEqual(self.tester.dn_attrs, new_state) |
| 610 | + self.assertEqual(tester2.dn_attrs, new_state) |
| 611 | + |
| 612 | + # self.tester seems to have been unbound by the time |
| 613 | + # self.addCleanup callbacks get called? Cleanup manually... |
| 614 | + self.tester.delete_s("cn=server1,%s" % self.suffix) |
| 615 | + self.tester.delete_s("cn=server2,%s" % self.suffix) |
| 616 | + |
| 617 | + |
445 | 618 | class DecodeSyncreplProtoTests(unittest.TestCase):
|
446 | 619 | """
|
447 | 620 | Tests of the ASN.1 decoder for tricky cases or past issues to ensure that
|
|
0 commit comments