ticket47573_test.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. '''
  2. Created on Nov 7, 2013
  3. @author: tbordaz
  4. '''
  5. import os
  6. import sys
  7. import time
  8. import ldap
  9. import logging
  10. import socket
  11. import time
  12. import logging
  13. import pytest
  14. import re
  15. from lib389 import DirSrv, Entry, tools
  16. from lib389.tools import DirSrvTools
  17. from lib389._constants import *
  18. from lib389.properties import *
  19. from constants import *
  20. logging.getLogger(__name__).setLevel(logging.DEBUG)
  21. log = logging.getLogger(__name__)
  22. installation_prefix = None
  23. TEST_REPL_DN = "cn=test_repl, %s" % SUFFIX
  24. ENTRY_DN = "cn=test_entry, %s" % SUFFIX
  25. MUST_OLD = "(postalAddress $ preferredLocale $ telexNumber)"
  26. MAY_OLD = "(postalCode $ street)"
  27. MUST_NEW = "(postalAddress $ preferredLocale)"
  28. MAY_NEW = "(telexNumber $ postalCode $ street)"
  29. class TopologyMasterConsumer(object):
  30. def __init__(self, master, consumer):
  31. master.open()
  32. self.master = master
  33. consumer.open()
  34. self.consumer = consumer
  35. def pattern_errorlog(file, log_pattern):
  36. try:
  37. pattern_errorlog.last_pos += 1
  38. except AttributeError:
  39. pattern_errorlog.last_pos = 0
  40. found = None
  41. log.debug("_pattern_errorlog: start at offset %d" % pattern_errorlog.last_pos)
  42. file.seek(pattern_errorlog.last_pos)
  43. # Use a while true iteration because 'for line in file: hit a
  44. # python bug that break file.tell()
  45. while True:
  46. line = file.readline()
  47. log.debug("_pattern_errorlog: [%d] %s" % (file.tell(), line))
  48. found = log_pattern.search(line)
  49. if ((line == '') or (found)):
  50. break
  51. log.debug("_pattern_errorlog: end at offset %d" % file.tell())
  52. pattern_errorlog.last_pos = file.tell()
  53. return found
  54. def _oc_definition(oid_ext, name, must=None, may=None):
  55. oid = "1.2.3.4.5.6.7.8.9.10.%d" % oid_ext
  56. desc = 'To test ticket 47573'
  57. sup = 'person'
  58. if not must:
  59. must = MUST_OLD
  60. if not may:
  61. may = MAY_OLD
  62. new_oc = "( %s NAME '%s' DESC '%s' SUP %s AUXILIARY MUST %s MAY %s )" % (oid, name, desc, sup, must, may)
  63. return new_oc
  64. def add_OC(instance, oid_ext, name):
  65. new_oc = _oc_definition(oid_ext, name)
  66. instance.schema.add_schema('objectClasses', new_oc)
  67. def mod_OC(instance, oid_ext, name, old_must=None, old_may=None, new_must=None, new_may=None):
  68. old_oc = _oc_definition(oid_ext, name, old_must, old_may)
  69. new_oc = _oc_definition(oid_ext, name, new_must, new_may)
  70. instance.schema.del_schema('objectClasses', old_oc)
  71. instance.schema.add_schema('objectClasses', new_oc)
  72. def trigger_schema_push(topology):
  73. """
  74. It triggers an update on the supplier. This will start a replication
  75. session and a schema push
  76. """
  77. try:
  78. trigger_schema_push.value += 1
  79. except AttributeError:
  80. trigger_schema_push.value = 1
  81. replace = [(ldap.MOD_REPLACE, 'telephonenumber', str(trigger_schema_push.value))]
  82. topology.master.modify_s(ENTRY_DN, replace)
  83. # wait 10 seconds that the update is replicated
  84. loop = 0
  85. while loop <= 10:
  86. try:
  87. ent = topology.consumer.getEntry(ENTRY_DN, ldap.SCOPE_BASE, "(objectclass=*)", ['telephonenumber'])
  88. val = ent.telephonenumber or "0"
  89. if int(val) == trigger_schema_push.value:
  90. return
  91. # the expected value is not yet replicated. try again
  92. time.sleep(1)
  93. loop += 1
  94. log.debug("trigger_schema_push: receive %s (expected %d)" % (val, trigger_schema_push.value))
  95. except ldap.NO_SUCH_OBJECT:
  96. time.sleep(1)
  97. loop += 1
  98. @pytest.fixture(scope="module")
  99. def topology(request):
  100. '''
  101. This fixture is used to create a replicated topology for the 'module'.
  102. The replicated topology is MASTER -> Consumer.
  103. At the beginning, It may exists a master instance and/or a consumer instance.
  104. It may also exists a backup for the master and/or the consumer.
  105. Principle:
  106. If master instance exists:
  107. restart it
  108. If consumer instance exists:
  109. restart it
  110. If backup of master AND backup of consumer exists:
  111. create or rebind to consumer
  112. create or rebind to master
  113. restore master from backup
  114. restore consumer from backup
  115. else:
  116. Cleanup everything
  117. remove instances
  118. remove backups
  119. Create instances
  120. Initialize replication
  121. Create backups
  122. '''
  123. global installation_prefix
  124. if installation_prefix:
  125. args_instance[SER_DEPLOYED_DIR] = installation_prefix
  126. master = DirSrv(verbose=False)
  127. consumer = DirSrv(verbose=False)
  128. # Args for the master instance
  129. args_instance[SER_HOST] = HOST_MASTER
  130. args_instance[SER_PORT] = PORT_MASTER
  131. args_instance[SER_SERVERID_PROP] = SERVERID_MASTER
  132. args_master = args_instance.copy()
  133. master.allocate(args_master)
  134. # Args for the consumer instance
  135. args_instance[SER_HOST] = HOST_CONSUMER
  136. args_instance[SER_PORT] = PORT_CONSUMER
  137. args_instance[SER_SERVERID_PROP] = SERVERID_CONSUMER
  138. args_consumer = args_instance.copy()
  139. consumer.allocate(args_consumer)
  140. # Get the status of the backups
  141. backup_master = master.checkBackupFS()
  142. backup_consumer = consumer.checkBackupFS()
  143. # Get the status of the instance and restart it if it exists
  144. instance_master = master.exists()
  145. if instance_master:
  146. master.stop(timeout=10)
  147. master.start(timeout=10)
  148. instance_consumer = consumer.exists()
  149. if instance_consumer:
  150. consumer.stop(timeout=10)
  151. consumer.start(timeout=10)
  152. if backup_master and backup_consumer:
  153. # The backups exist, assuming they are correct
  154. # we just re-init the instances with them
  155. if not instance_master:
  156. master.create()
  157. # Used to retrieve configuration information (dbdir, confdir...)
  158. master.open()
  159. if not instance_consumer:
  160. consumer.create()
  161. # Used to retrieve configuration information (dbdir, confdir...)
  162. consumer.open()
  163. # restore master from backup
  164. master.stop(timeout=10)
  165. master.restoreFS(backup_master)
  166. master.start(timeout=10)
  167. # restore consumer from backup
  168. consumer.stop(timeout=10)
  169. consumer.restoreFS(backup_consumer)
  170. consumer.start(timeout=10)
  171. else:
  172. # We should be here only in two conditions
  173. # - This is the first time a test involve master-consumer
  174. # so we need to create everything
  175. # - Something weird happened (instance/backup destroyed)
  176. # so we discard everything and recreate all
  177. # Remove all the backups. So even if we have a specific backup file
  178. # (e.g backup_master) we clear all backups that an instance my have created
  179. if backup_master:
  180. master.clearBackupFS()
  181. if backup_consumer:
  182. consumer.clearBackupFS()
  183. # Remove all the instances
  184. if instance_master:
  185. master.delete()
  186. if instance_consumer:
  187. consumer.delete()
  188. # Create the instances
  189. master.create()
  190. master.open()
  191. consumer.create()
  192. consumer.open()
  193. #
  194. # Now prepare the Master-Consumer topology
  195. #
  196. # First Enable replication
  197. master.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_MASTER, replicaId=REPLICAID_MASTER)
  198. consumer.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_CONSUMER)
  199. # Initialize the supplier->consumer
  200. properties = {RA_NAME: r'meTo_$host:$port',
  201. RA_BINDDN: defaultProperties[REPLICATION_BIND_DN],
  202. RA_BINDPW: defaultProperties[REPLICATION_BIND_PW],
  203. RA_METHOD: defaultProperties[REPLICATION_BIND_METHOD],
  204. RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
  205. repl_agreement = master.agreement.create(suffix=SUFFIX, host=consumer.host, port=consumer.port, properties=properties)
  206. if not repl_agreement:
  207. log.fatal("Fail to create a replica agreement")
  208. sys.exit(1)
  209. log.debug("%s created" % repl_agreement)
  210. master.agreement.init(SUFFIX, HOST_CONSUMER, PORT_CONSUMER)
  211. master.waitForReplInit(repl_agreement)
  212. # Check replication is working fine
  213. master.add_s(Entry((TEST_REPL_DN, {
  214. 'objectclass': "top person".split(),
  215. 'sn': 'test_repl',
  216. 'cn': 'test_repl'})))
  217. loop = 0
  218. while loop <= 10:
  219. try:
  220. ent = consumer.getEntry(TEST_REPL_DN, ldap.SCOPE_BASE, "(objectclass=*)")
  221. break
  222. except ldap.NO_SUCH_OBJECT:
  223. time.sleep(1)
  224. loop += 1
  225. # Time to create the backups
  226. master.stop(timeout=10)
  227. master.backupfile = master.backupFS()
  228. master.start(timeout=10)
  229. consumer.stop(timeout=10)
  230. consumer.backupfile = consumer.backupFS()
  231. consumer.start(timeout=10)
  232. # clear the tmp directory
  233. master.clearTmpDir(__file__)
  234. #
  235. # Here we have two instances master and consumer
  236. # with replication working. Either coming from a backup recovery
  237. # or from a fresh (re)init
  238. # Time to return the topology
  239. return TopologyMasterConsumer(master, consumer)
  240. def test_ticket47573_init(topology):
  241. """
  242. Initialize the test environment
  243. """
  244. log.debug("test_ticket47573_init topology %r (master %r, consumer %r" % (topology, topology.master, topology.consumer))
  245. # the test case will check if a warning message is logged in the
  246. # error log of the supplier
  247. topology.master.errorlog_file = open(topology.master.errlog, "r")
  248. # This entry will be used to trigger attempt of schema push
  249. topology.master.add_s(Entry((ENTRY_DN, {
  250. 'objectclass': "top person".split(),
  251. 'sn': 'test_entry',
  252. 'cn': 'test_entry'})))
  253. def test_ticket47573_one(topology):
  254. """
  255. Summary: Add a custom OC with MUST and MAY
  256. MUST = postalAddress $ preferredLocale
  257. MAY = telexNumber $ postalCode $ street
  258. Final state
  259. - supplier +OCwithMayAttr
  260. - consumer +OCwithMayAttr
  261. """
  262. log.debug("test_ticket47573_one topology %r (master %r, consumer %r" % (topology, topology.master, topology.consumer))
  263. # update the schema of the supplier so that it is a superset of
  264. # consumer. Schema should be pushed
  265. new_oc = _oc_definition(2, 'OCwithMayAttr',
  266. must = MUST_OLD,
  267. may = MAY_OLD)
  268. topology.master.schema.add_schema('objectClasses', new_oc)
  269. trigger_schema_push(topology)
  270. master_schema_csn = topology.master.schema.get_schema_csn()
  271. consumer_schema_csn = topology.consumer.schema.get_schema_csn()
  272. # Check the schemaCSN was updated on the consumer
  273. log.debug("test_ticket47573_one master_schema_csn=%s", master_schema_csn)
  274. log.debug("ctest_ticket47573_one onsumer_schema_csn=%s", consumer_schema_csn)
  275. assert master_schema_csn == consumer_schema_csn
  276. # Check the error log of the supplier does not contain an error
  277. regex = re.compile("must not be overwritten \(set replication log for additional info\)")
  278. res = pattern_errorlog(topology.master.errorlog_file, regex)
  279. assert res == None
  280. def test_ticket47573_two(topology):
  281. """
  282. Summary: Change OCwithMayAttr to move a MAY attribute to a MUST attribute
  283. Final state
  284. - supplier OCwithMayAttr updated
  285. - consumer OCwithMayAttr updated
  286. """
  287. # Update the objectclass so that a MAY attribute is moved to MUST attribute
  288. mod_OC(topology.master, 2, 'OCwithMayAttr', old_must=MUST_OLD, new_must=MUST_NEW, old_may=MAY_OLD, new_may=MAY_NEW)
  289. # now push the scheam
  290. trigger_schema_push(topology)
  291. master_schema_csn = topology.master.schema.get_schema_csn()
  292. consumer_schema_csn = topology.consumer.schema.get_schema_csn()
  293. # Check the schemaCSN was NOT updated on the consumer
  294. log.debug("test_ticket47573_two master_schema_csn=%s", master_schema_csn)
  295. log.debug("test_ticket47573_two consumer_schema_csn=%s", consumer_schema_csn)
  296. assert master_schema_csn == consumer_schema_csn
  297. # Check the error log of the supplier does not contain an error
  298. regex = re.compile("must not be overwritten \(set replication log for additional info\)")
  299. res = pattern_errorlog(topology.master.errorlog_file, regex)
  300. assert res == None
  301. def test_ticket47573_three(topology):
  302. '''
  303. Create a entry with OCwithMayAttr OC
  304. '''
  305. # Check replication is working fine
  306. dn = "cn=ticket47573, %s" % SUFFIX
  307. topology.master.add_s(Entry((dn,
  308. {'objectclass': "top person OCwithMayAttr".split(),
  309. 'sn': 'test_repl',
  310. 'cn': 'test_repl',
  311. 'postalAddress': 'here',
  312. 'preferredLocale': 'en',
  313. 'telexNumber': '12$us$21',
  314. 'postalCode': '54321'})))
  315. loop = 0
  316. while loop <= 10:
  317. try:
  318. ent = topology.consumer.getEntry(dn, ldap.SCOPE_BASE, "(objectclass=*)")
  319. break
  320. except ldap.NO_SUCH_OBJECT:
  321. time.sleep(1)
  322. loop += 1
  323. assert loop <= 10
  324. def test_ticket47573_final(topology):
  325. topology.master.stop(timeout=10)
  326. topology.consumer.stop(timeout=10)
  327. def run_isolated():
  328. '''
  329. run_isolated is used to run these test cases independently of a test scheduler (xunit, py.test..)
  330. To run isolated without py.test, you need to
  331. - edit this file and comment '@pytest.fixture' line before 'topology' function.
  332. - set the installation prefix
  333. - run this program
  334. '''
  335. global installation_prefix
  336. installation_prefix = None
  337. topo = topology(True)
  338. test_ticket47573_init(topo)
  339. test_ticket47573_one(topo)
  340. test_ticket47573_two(topo)
  341. test_ticket47573_three(topo)
  342. test_ticket47573_final(topo)
  343. if __name__ == '__main__':
  344. run_isolated()