ticket47787_test.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. '''
  2. Created on April 14, 2014
  3. @author: tbordaz
  4. '''
  5. import os
  6. import sys
  7. import time
  8. import ldap
  9. import logging
  10. import pytest
  11. import re
  12. from lib389 import DirSrv, Entry, tools, NoSuchEntryError
  13. from lib389.tools import DirSrvTools
  14. from lib389._constants import *
  15. from lib389.properties import *
  16. from lib389._constants import REPLICAROLE_MASTER
  17. logging.getLogger(__name__).setLevel(logging.DEBUG)
  18. log = logging.getLogger(__name__)
  19. #
  20. # important part. We can deploy Master1 and Master2 on different versions
  21. #
  22. installation1_prefix = None
  23. installation2_prefix = None
  24. # set this flag to False so that it will assert on failure _status_entry_both_server
  25. DEBUG_FLAG = False
  26. TEST_REPL_DN = "cn=test_repl, %s" % SUFFIX
  27. STAGING_CN = "staged user"
  28. PRODUCTION_CN = "accounts"
  29. EXCEPT_CN = "excepts"
  30. STAGING_DN = "cn=%s,%s" % (STAGING_CN, SUFFIX)
  31. PRODUCTION_DN = "cn=%s,%s" % (PRODUCTION_CN, SUFFIX)
  32. PROD_EXCEPT_DN = "cn=%s,%s" % (EXCEPT_CN, PRODUCTION_DN)
  33. STAGING_PATTERN = "cn=%s*,%s" % (STAGING_CN[:2], SUFFIX)
  34. PRODUCTION_PATTERN = "cn=%s*,%s" % (PRODUCTION_CN[:2], SUFFIX)
  35. BAD_STAGING_PATTERN = "cn=bad*,%s" % (SUFFIX)
  36. BAD_PRODUCTION_PATTERN = "cn=bad*,%s" % (SUFFIX)
  37. BIND_CN = "bind_entry"
  38. BIND_DN = "cn=%s,%s" % (BIND_CN, SUFFIX)
  39. BIND_PW = "password"
  40. NEW_ACCOUNT = "new_account"
  41. MAX_ACCOUNTS = 20
  42. CONFIG_MODDN_ACI_ATTR = "nsslapd-moddn-aci"
  43. class TopologyMaster1Master2(object):
  44. def __init__(self, master1, master2):
  45. master1.open()
  46. self.master1 = master1
  47. master2.open()
  48. self.master2 = master2
  49. @pytest.fixture(scope="module")
  50. def topology(request):
  51. '''
  52. This fixture is used to create a replicated topology for the 'module'.
  53. The replicated topology is MASTER1 <-> Master2.
  54. '''
  55. global installation1_prefix
  56. global installation2_prefix
  57. # allocate master1 on a given deployement
  58. master1 = DirSrv(verbose=False)
  59. if installation1_prefix:
  60. args_instance[SER_DEPLOYED_DIR] = installation1_prefix
  61. # Args for the master1 instance
  62. args_instance[SER_HOST] = HOST_MASTER_1
  63. args_instance[SER_PORT] = PORT_MASTER_1
  64. args_instance[SER_SERVERID_PROP] = SERVERID_MASTER_1
  65. args_master = args_instance.copy()
  66. master1.allocate(args_master)
  67. # allocate master1 on a given deployement
  68. master2 = DirSrv(verbose=False)
  69. if installation2_prefix:
  70. args_instance[SER_DEPLOYED_DIR] = installation2_prefix
  71. # Args for the consumer instance
  72. args_instance[SER_HOST] = HOST_MASTER_2
  73. args_instance[SER_PORT] = PORT_MASTER_2
  74. args_instance[SER_SERVERID_PROP] = SERVERID_MASTER_2
  75. args_master = args_instance.copy()
  76. master2.allocate(args_master)
  77. # Get the status of the instance and restart it if it exists
  78. instance_master1 = master1.exists()
  79. instance_master2 = master2.exists()
  80. # Remove all the instances
  81. if instance_master1:
  82. master1.delete()
  83. if instance_master2:
  84. master2.delete()
  85. # Create the instances
  86. master1.create()
  87. master1.open()
  88. master2.create()
  89. master2.open()
  90. #
  91. # Now prepare the Master-Consumer topology
  92. #
  93. # First Enable replication
  94. master1.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_MASTER, replicaId=REPLICAID_MASTER_1)
  95. master2.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_MASTER, replicaId=REPLICAID_MASTER_2)
  96. # Initialize the supplier->consumer
  97. properties = {RA_NAME: r'meTo_$host:$port',
  98. RA_BINDDN: defaultProperties[REPLICATION_BIND_DN],
  99. RA_BINDPW: defaultProperties[REPLICATION_BIND_PW],
  100. RA_METHOD: defaultProperties[REPLICATION_BIND_METHOD],
  101. RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
  102. repl_agreement = master1.agreement.create(suffix=SUFFIX, host=master2.host, port=master2.port, properties=properties)
  103. if not repl_agreement:
  104. log.fatal("Fail to create a replica agreement")
  105. sys.exit(1)
  106. log.debug("%s created" % repl_agreement)
  107. properties = {RA_NAME: r'meTo_$host:$port',
  108. RA_BINDDN: defaultProperties[REPLICATION_BIND_DN],
  109. RA_BINDPW: defaultProperties[REPLICATION_BIND_PW],
  110. RA_METHOD: defaultProperties[REPLICATION_BIND_METHOD],
  111. RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
  112. master2.agreement.create(suffix=SUFFIX, host=master1.host, port=master1.port, properties=properties)
  113. master1.agreement.init(SUFFIX, HOST_MASTER_2, PORT_MASTER_2)
  114. master1.waitForReplInit(repl_agreement)
  115. # Check replication is working fine
  116. if master1.testReplication(DEFAULT_SUFFIX, master2):
  117. log.info('Replication is working.')
  118. else:
  119. log.fatal('Replication is not working.')
  120. assert False
  121. # clear the tmp directory
  122. master1.clearTmpDir(__file__)
  123. # Here we have two instances master and consumer
  124. # with replication working.
  125. return TopologyMaster1Master2(master1, master2)
  126. def _bind_manager(server):
  127. server.log.info("Bind as %s " % DN_DM)
  128. server.simple_bind_s(DN_DM, PASSWORD)
  129. def _bind_normal(server):
  130. server.log.info("Bind as %s " % BIND_DN)
  131. server.simple_bind_s(BIND_DN, BIND_PW)
  132. def _header(topology, label):
  133. topology.master1.log.info("\n\n###############################################")
  134. topology.master1.log.info("#######")
  135. topology.master1.log.info("####### %s" % label)
  136. topology.master1.log.info("#######")
  137. topology.master1.log.info("###############################################")
  138. def _status_entry_both_server(topology, name=None, desc=None, debug=True):
  139. if not name:
  140. return
  141. topology.master1.log.info("\n\n######################### Tombstone on M1 ######################\n")
  142. attr = 'description'
  143. found = False
  144. attempt = 0
  145. while not found and attempt < 10:
  146. ent_m1 = _find_tombstone(topology.master1, SUFFIX, 'sn', name)
  147. if attr in ent_m1.getAttrs():
  148. found = True
  149. else:
  150. time.sleep(1)
  151. attempt = attempt + 1
  152. assert ent_m1
  153. topology.master1.log.info("\n\n######################### Tombstone on M2 ######################\n")
  154. ent_m2 = _find_tombstone(topology.master2, SUFFIX, 'sn', name)
  155. assert ent_m2
  156. topology.master1.log.info("\n\n######################### Description ######################\n%s\n" % desc)
  157. topology.master1.log.info("M1 only\n")
  158. for attr in ent_m1.getAttrs():
  159. if not debug:
  160. assert attr in ent_m2.getAttrs()
  161. if not attr in ent_m2.getAttrs():
  162. topology.master1.log.info(" %s" % attr)
  163. for val in ent_m1.getValues(attr):
  164. topology.master1.log.info(" %s" % val)
  165. topology.master1.log.info("M2 only\n")
  166. for attr in ent_m2.getAttrs():
  167. if not debug:
  168. assert attr in ent_m1.getAttrs()
  169. if not attr in ent_m1.getAttrs():
  170. topology.master1.log.info(" %s" % attr)
  171. for val in ent_m2.getValues(attr):
  172. topology.master1.log.info(" %s" % val)
  173. topology.master1.log.info("M1 differs M2\n")
  174. if not debug:
  175. assert ent_m1.dn == ent_m2.dn
  176. if ent_m1.dn != ent_m2.dn:
  177. topology.master1.log.info(" M1[dn] = %s\n M2[dn] = %s" % (ent_m1.dn, ent_m2.dn))
  178. for attr1 in ent_m1.getAttrs():
  179. if attr1 in ent_m2.getAttrs():
  180. for val1 in ent_m1.getValues(attr1):
  181. found = False
  182. for val2 in ent_m2.getValues(attr1):
  183. if val1 == val2:
  184. found = True
  185. break
  186. if not debug:
  187. assert found
  188. if not found:
  189. topology.master1.log.info(" M1[%s] = %s" % (attr1, val1))
  190. for attr2 in ent_m2.getAttrs():
  191. if attr2 in ent_m1.getAttrs():
  192. for val2 in ent_m2.getValues(attr2):
  193. found = False
  194. for val1 in ent_m1.getValues(attr2):
  195. if val2 == val1:
  196. found = True
  197. break
  198. if not debug:
  199. assert found
  200. if not found:
  201. topology.master1.log.info(" M2[%s] = %s" % (attr2, val2))
  202. def _pause_RAs(topology):
  203. topology.master1.log.info("\n\n######################### Pause RA M1<->M2 ######################\n")
  204. ents = topology.master1.agreement.list(suffix=SUFFIX)
  205. assert len(ents) == 1
  206. topology.master1.agreement.pause(ents[0].dn)
  207. ents = topology.master2.agreement.list(suffix=SUFFIX)
  208. assert len(ents) == 1
  209. topology.master2.agreement.pause(ents[0].dn)
  210. def _resume_RAs(topology):
  211. topology.master1.log.info("\n\n######################### resume RA M1<->M2 ######################\n")
  212. ents = topology.master1.agreement.list(suffix=SUFFIX)
  213. assert len(ents) == 1
  214. topology.master1.agreement.resume(ents[0].dn)
  215. ents = topology.master2.agreement.list(suffix=SUFFIX)
  216. assert len(ents) == 1
  217. topology.master2.agreement.resume(ents[0].dn)
  218. def _find_tombstone(instance, base, attr, value):
  219. #
  220. # we can not use a filter with a (&(objeclass=nsTombstone)(sn=name)) because
  221. # tombstone are not index in 'sn' so 'sn=name' will return NULL
  222. # and even if tombstone are indexed for objectclass the '&' will set
  223. # the candidate list to NULL
  224. #
  225. filt = '(objectclass=%s)' % REPLICA_OC_TOMBSTONE
  226. ents = instance.search_s(base, ldap.SCOPE_SUBTREE, filt)
  227. #found = False
  228. for ent in ents:
  229. if ent.hasAttr(attr):
  230. for val in ent.getValues(attr):
  231. if val == value:
  232. instance.log.debug("tombstone found: %r" % ent)
  233. return ent
  234. return None
  235. def _delete_entry(instance, entry_dn, name):
  236. instance.log.info("\n\n######################### DELETE %s (M1) ######################\n" % name)
  237. # delete the entry
  238. instance.delete_s(entry_dn)
  239. assert _find_tombstone(instance, SUFFIX, 'sn', name) is not None
  240. def _mod_entry(instance, entry_dn, attr, value):
  241. instance.log.info("\n\n######################### MOD %s (M2) ######################\n" % entry_dn)
  242. mod = [(ldap.MOD_REPLACE, attr, value)]
  243. instance.modify_s(entry_dn, mod)
  244. def _modrdn_entry(instance=None, entry_dn=None, new_rdn=None, del_old=0, new_superior=None):
  245. assert instance is not None
  246. assert entry_dn is not None
  247. if not new_rdn:
  248. pattern = 'cn=(.*),(.*)'
  249. rdnre = re.compile(pattern)
  250. match = rdnre.match(entry_dn)
  251. old_value = match.group(1)
  252. new_rdn_val = "%s_modrdn" % old_value
  253. new_rdn = "cn=%s" % new_rdn_val
  254. instance.log.info("\n\n######################### MODRDN %s (M2) ######################\n" % new_rdn)
  255. if new_superior:
  256. instance.rename_s(entry_dn, new_rdn, newsuperior=new_superior, delold=del_old)
  257. else:
  258. instance.rename_s(entry_dn, new_rdn, delold=del_old)
  259. def _check_entry_exists(instance, entry_dn):
  260. loop = 0
  261. ent = None
  262. while loop <= 10:
  263. try:
  264. ent = instance.getEntry(entry_dn, ldap.SCOPE_BASE, "(objectclass=*)")
  265. break
  266. except ldap.NO_SUCH_OBJECT:
  267. time.sleep(1)
  268. loop += 1
  269. if ent is None:
  270. assert False
  271. def _check_mod_received(instance, base, filt, attr, value):
  272. instance.log.info("\n\n######################### Check MOD replicated on %s ######################\n" % instance.serverid)
  273. loop = 0
  274. while loop <= 10:
  275. ent = instance.getEntry(base, ldap.SCOPE_SUBTREE, filt)
  276. if ent.hasAttr(attr) and ent.getValue(attr) == value:
  277. break
  278. time.sleep(1)
  279. loop += 1
  280. assert loop <= 10
  281. def _check_replication(topology, entry_dn):
  282. # prepare the filter to retrieve the entry
  283. filt = entry_dn.split(',')[0]
  284. topology.master1.log.info("\n######################### Check replicat M1->M2 ######################\n")
  285. loop = 0
  286. while loop <= 10:
  287. attr = 'description'
  288. value = 'test_value_%d' % loop
  289. mod = [(ldap.MOD_REPLACE, attr, value)]
  290. topology.master1.modify_s(entry_dn, mod)
  291. _check_mod_received(topology.master2, SUFFIX, filt, attr, value)
  292. loop += 1
  293. topology.master1.log.info("\n######################### Check replicat M2->M1 ######################\n")
  294. loop = 0
  295. while loop <= 10:
  296. attr = 'description'
  297. value = 'test_value_%d' % loop
  298. mod = [(ldap.MOD_REPLACE, attr, value)]
  299. topology.master2.modify_s(entry_dn, mod)
  300. _check_mod_received(topology.master1, SUFFIX, filt, attr, value)
  301. loop += 1
  302. def test_ticket47787_init(topology):
  303. """
  304. Creates
  305. - a staging DIT
  306. - a production DIT
  307. - add accounts in staging DIT
  308. """
  309. topology.master1.log.info("\n\n######################### INITIALIZATION ######################\n")
  310. # entry used to bind with
  311. topology.master1.log.info("Add %s" % BIND_DN)
  312. topology.master1.add_s(Entry((BIND_DN, {
  313. 'objectclass': "top person".split(),
  314. 'sn': BIND_CN,
  315. 'cn': BIND_CN,
  316. 'userpassword': BIND_PW})))
  317. # DIT for staging
  318. topology.master1.log.info("Add %s" % STAGING_DN)
  319. topology.master1.add_s(Entry((STAGING_DN, {
  320. 'objectclass': "top organizationalRole".split(),
  321. 'cn': STAGING_CN,
  322. 'description': "staging DIT"})))
  323. # DIT for production
  324. topology.master1.log.info("Add %s" % PRODUCTION_DN)
  325. topology.master1.add_s(Entry((PRODUCTION_DN, {
  326. 'objectclass': "top organizationalRole".split(),
  327. 'cn': PRODUCTION_CN,
  328. 'description': "production DIT"})))
  329. # enable replication error logging
  330. mod = [(ldap.MOD_REPLACE, 'nsslapd-errorlog-level', '8192')]
  331. topology.master1.modify_s(DN_CONFIG, mod)
  332. topology.master2.modify_s(DN_CONFIG, mod)
  333. # add dummy entries in the staging DIT
  334. for cpt in range(MAX_ACCOUNTS):
  335. name = "%s%d" % (NEW_ACCOUNT, cpt)
  336. topology.master1.add_s(Entry(("cn=%s,%s" % (name, STAGING_DN), {
  337. 'objectclass': "top person".split(),
  338. 'sn': name,
  339. 'cn': name})))
  340. def test_ticket47787_2(topology):
  341. '''
  342. Disable replication so that updates are not replicated
  343. Delete an entry on M1. Modrdn it on M2 (chg rdn + delold=0 + same superior).
  344. update a test entry on M2
  345. Reenable the RA.
  346. checks that entry was deleted on M2 (with the modified RDN)
  347. checks that test entry was replicated on M1 (replication M2->M1 not broken by modrdn)
  348. '''
  349. _header(topology, "test_ticket47787_2")
  350. _bind_manager(topology.master1)
  351. _bind_manager(topology.master2)
  352. #entry to test the replication is still working
  353. name = "%s%d" % (NEW_ACCOUNT, MAX_ACCOUNTS - 1)
  354. test_rdn = "cn=%s" % (name)
  355. testentry_dn = "%s,%s" % (test_rdn, STAGING_DN)
  356. name = "%s%d" % (NEW_ACCOUNT, MAX_ACCOUNTS - 2)
  357. test2_rdn = "cn=%s" % (name)
  358. testentry2_dn = "%s,%s" % (test2_rdn, STAGING_DN)
  359. # value of updates to test the replication both ways
  360. attr = 'description'
  361. value = 'test_ticket47787_2'
  362. # entry for the modrdn
  363. name = "%s%d" % (NEW_ACCOUNT, 1)
  364. rdn = "cn=%s" % (name)
  365. entry_dn = "%s,%s" % (rdn, STAGING_DN)
  366. # created on M1, wait the entry exists on M2
  367. _check_entry_exists(topology.master2, entry_dn)
  368. _check_entry_exists(topology.master2, testentry_dn)
  369. _pause_RAs(topology)
  370. # Delete 'entry_dn' on M1.
  371. # dummy update is only have a first CSN before the DEL
  372. # else the DEL will be in min_csn RUV and make diagnostic a bit more complex
  373. _mod_entry(topology.master1, testentry2_dn, attr, 'dummy')
  374. _delete_entry(topology.master1, entry_dn, name)
  375. _mod_entry(topology.master1, testentry2_dn, attr, value)
  376. time.sleep(1) # important to have MOD.csn != DEL.csn
  377. # MOD 'entry_dn' on M1.
  378. # dummy update is only have a first CSN before the MOD entry_dn
  379. # else the DEL will be in min_csn RUV and make diagnostic a bit more complex
  380. _mod_entry(topology.master2, testentry_dn, attr, 'dummy')
  381. _mod_entry(topology.master2, entry_dn, attr, value)
  382. _mod_entry(topology.master2, testentry_dn, attr, value)
  383. _resume_RAs(topology)
  384. topology.master1.log.info("\n\n######################### Check DEL replicated on M2 ######################\n")
  385. loop = 0
  386. while loop <= 10:
  387. ent = _find_tombstone(topology.master2, SUFFIX, 'sn', name)
  388. if ent:
  389. break
  390. time.sleep(1)
  391. loop += 1
  392. assert loop <= 10
  393. assert ent
  394. # the following checks are not necessary
  395. # as this bug is only for failing replicated MOD (entry_dn) on M1
  396. #_check_mod_received(topology.master1, SUFFIX, "(%s)" % (test_rdn), attr, value)
  397. #_check_mod_received(topology.master2, SUFFIX, "(%s)" % (test2_rdn), attr, value)
  398. #
  399. #_check_replication(topology, testentry_dn)
  400. _status_entry_both_server(topology, name=name, desc="DEL M1 - MOD M2", debug=DEBUG_FLAG)
  401. topology.master1.log.info("\n\n######################### Check MOD replicated on M1 ######################\n")
  402. loop = 0
  403. while loop <= 10:
  404. ent = _find_tombstone(topology.master1, SUFFIX, 'sn', name)
  405. if ent:
  406. break
  407. time.sleep(1)
  408. loop += 1
  409. assert loop <= 10
  410. assert ent
  411. assert ent.hasAttr(attr)
  412. assert ent.getValue(attr) == value
  413. def test_ticket47787_final(topology):
  414. topology.master1.delete()
  415. topology.master2.delete()
  416. log.info('Testcase PASSED')
  417. def run_isolated():
  418. '''
  419. run_isolated is used to run these test cases independently of a test scheduler (xunit, py.test..)
  420. To run isolated without py.test, you need to
  421. - edit this file and comment '@pytest.fixture' line before 'topology' function.
  422. - set the installation prefix
  423. - run this program
  424. '''
  425. global installation1_prefix
  426. global installation2_prefix
  427. installation1_prefix = None
  428. installation2_prefix = None
  429. topo = topology(True)
  430. topo.master1.log.info("\n\n######################### Ticket 47787 ######################\n")
  431. test_ticket47787_init(topo)
  432. test_ticket47787_2(topo)
  433. test_ticket47787_final(topo)
  434. if __name__ == '__main__':
  435. run_isolated()