ticket47573_test.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  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. #
  233. # Here we have two instances master and consumer
  234. # with replication working. Either coming from a backup recovery
  235. # or from a fresh (re)init
  236. # Time to return the topology
  237. return TopologyMasterConsumer(master, consumer)
  238. def test_ticket47573_init(topology):
  239. """
  240. Initialize the test environment
  241. """
  242. log.debug("test_ticket47573_init topology %r (master %r, consumer %r" % (topology, topology.master, topology.consumer))
  243. # the test case will check if a warning message is logged in the
  244. # error log of the supplier
  245. topology.master.errorlog_file = open(topology.master.errlog, "r")
  246. # This entry will be used to trigger attempt of schema push
  247. topology.master.add_s(Entry((ENTRY_DN, {
  248. 'objectclass': "top person".split(),
  249. 'sn': 'test_entry',
  250. 'cn': 'test_entry'})))
  251. def test_ticket47573_one(topology):
  252. """
  253. Summary: Add a custom OC with MUST and MAY
  254. MUST = postalAddress $ preferredLocale
  255. MAY = telexNumber $ postalCode $ street
  256. Final state
  257. - supplier +OCwithMayAttr
  258. - consumer +OCwithMayAttr
  259. """
  260. log.debug("test_ticket47573_one topology %r (master %r, consumer %r" % (topology, topology.master, topology.consumer))
  261. # update the schema of the supplier so that it is a superset of
  262. # consumer. Schema should be pushed
  263. new_oc = _oc_definition(2, 'OCwithMayAttr',
  264. must = MUST_OLD,
  265. may = MAY_OLD)
  266. topology.master.schema.add_schema('objectClasses', new_oc)
  267. trigger_schema_push(topology)
  268. master_schema_csn = topology.master.schema.get_schema_csn()
  269. consumer_schema_csn = topology.consumer.schema.get_schema_csn()
  270. # Check the schemaCSN was updated on the consumer
  271. log.debug("test_ticket47573_one master_schema_csn=%s", master_schema_csn)
  272. log.debug("ctest_ticket47573_one onsumer_schema_csn=%s", consumer_schema_csn)
  273. assert master_schema_csn == consumer_schema_csn
  274. # Check the error log of the supplier does not contain an error
  275. regex = re.compile("must not be overwritten \(set replication log for additional info\)")
  276. res = pattern_errorlog(topology.master.errorlog_file, regex)
  277. assert res == None
  278. def test_ticket47573_two(topology):
  279. """
  280. Summary: Change OCwithMayAttr to move a MAY attribute to a MUST attribute
  281. Final state
  282. - supplier OCwithMayAttr updated
  283. - consumer OCwithMayAttr updated
  284. """
  285. # Update the objectclass so that a MAY attribute is moved to MUST attribute
  286. mod_OC(topology.master, 2, 'OCwithMayAttr', old_must=MUST_OLD, new_must=MUST_NEW, old_may=MAY_OLD, new_may=MAY_NEW)
  287. # now push the scheam
  288. trigger_schema_push(topology)
  289. master_schema_csn = topology.master.schema.get_schema_csn()
  290. consumer_schema_csn = topology.consumer.schema.get_schema_csn()
  291. # Check the schemaCSN was NOT updated on the consumer
  292. log.debug("test_ticket47573_two master_schema_csn=%s", master_schema_csn)
  293. log.debug("test_ticket47573_two consumer_schema_csn=%s", consumer_schema_csn)
  294. assert master_schema_csn == consumer_schema_csn
  295. # Check the error log of the supplier does not contain an error
  296. regex = re.compile("must not be overwritten \(set replication log for additional info\)")
  297. res = pattern_errorlog(topology.master.errorlog_file, regex)
  298. assert res == None
  299. def test_ticket47573_three(topology):
  300. '''
  301. Create a entry with OCwithMayAttr OC
  302. '''
  303. # Check replication is working fine
  304. dn = "cn=ticket47573, %s" % SUFFIX
  305. topology.master.add_s(Entry((dn,
  306. {'objectclass': "top person OCwithMayAttr".split(),
  307. 'sn': 'test_repl',
  308. 'cn': 'test_repl',
  309. 'postalAddress': 'here',
  310. 'preferredLocale': 'en',
  311. 'telexNumber': '12$us$21',
  312. 'postalCode': '54321'})))
  313. loop = 0
  314. while loop <= 10:
  315. try:
  316. ent = topology.consumer.getEntry(dn, ldap.SCOPE_BASE, "(objectclass=*)")
  317. break
  318. except ldap.NO_SUCH_OBJECT:
  319. time.sleep(1)
  320. loop += 1
  321. assert loop <= 10
  322. def test_ticket47573_final(topology):
  323. topology.master.stop(timeout=10)
  324. topology.consumer.stop(timeout=10)
  325. def run_isolated():
  326. '''
  327. run_isolated is used to run these test cases independently of a test scheduler (xunit, py.test..)
  328. To run isolated without py.test, you need to
  329. - edit this file and comment '@pytest.fixture' line before 'topology' function.
  330. - set the installation prefix
  331. - run this program
  332. '''
  333. global installation_prefix
  334. installation_prefix = None
  335. topo = topology(True)
  336. test_ticket47573_init(topo)
  337. test_ticket47573_one(topo)
  338. test_ticket47573_two(topo)
  339. test_ticket47573_three(topo)
  340. test_ticket47573_final(topo)
  341. if __name__ == '__main__':
  342. run_isolated()