bind10_test.py.in 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. # Copyright (C) 2011 Internet Systems Consortium.
  2. #
  3. # Permission to use, copy, modify, and distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  8. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  9. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  10. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  11. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  12. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  13. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  14. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. from bind10_src import ProcessInfo, BoB, parse_args, dump_pid, unlink_pid_file, _BASETIME
  16. from isc.bind10.component import Component
  17. # XXX: environment tests are currently disabled, due to the preprocessor
  18. # setup that we have now complicating the environment
  19. import unittest
  20. import sys
  21. import os
  22. import signal
  23. import socket
  24. from isc.net.addr import IPAddr
  25. import time
  26. import isc
  27. import isc.log
  28. from isc.testutils.parse_args import TestOptParser, OptsError
  29. class TestProcessInfo(unittest.TestCase):
  30. def setUp(self):
  31. # redirect stdout to a pipe so we can check that our
  32. # process spawning is doing the right thing with stdout
  33. self.old_stdout = os.dup(sys.stdout.fileno())
  34. self.pipes = os.pipe()
  35. os.dup2(self.pipes[1], sys.stdout.fileno())
  36. os.close(self.pipes[1])
  37. # note that we use dup2() to restore the original stdout
  38. # to the main program ASAP in each test... this prevents
  39. # hangs reading from the child process (as the pipe is only
  40. # open in the child), and also insures nice pretty output
  41. def tearDown(self):
  42. # clean up our stdout munging
  43. os.dup2(self.old_stdout, sys.stdout.fileno())
  44. os.close(self.pipes[0])
  45. def test_init(self):
  46. pi = ProcessInfo('Test Process', [ '/bin/echo', 'foo' ])
  47. pi.spawn()
  48. os.dup2(self.old_stdout, sys.stdout.fileno())
  49. self.assertEqual(pi.name, 'Test Process')
  50. self.assertEqual(pi.args, [ '/bin/echo', 'foo' ])
  51. # self.assertEqual(pi.env, { 'PATH': os.environ['PATH'],
  52. # 'PYTHON_EXEC': os.environ['PYTHON_EXEC'] })
  53. self.assertEqual(pi.dev_null_stdout, False)
  54. self.assertEqual(os.read(self.pipes[0], 100), b"foo\n")
  55. self.assertNotEqual(pi.process, None)
  56. self.assertTrue(type(pi.pid) is int)
  57. # def test_setting_env(self):
  58. # pi = ProcessInfo('Test Process', [ '/bin/true' ], env={'FOO': 'BAR'})
  59. # os.dup2(self.old_stdout, sys.stdout.fileno())
  60. # self.assertEqual(pi.env, { 'PATH': os.environ['PATH'],
  61. # 'PYTHON_EXEC': os.environ['PYTHON_EXEC'],
  62. # 'FOO': 'BAR' })
  63. def test_setting_null_stdout(self):
  64. pi = ProcessInfo('Test Process', [ '/bin/echo', 'foo' ],
  65. dev_null_stdout=True)
  66. pi.spawn()
  67. os.dup2(self.old_stdout, sys.stdout.fileno())
  68. self.assertEqual(pi.dev_null_stdout, True)
  69. self.assertEqual(os.read(self.pipes[0], 100), b"")
  70. def test_respawn(self):
  71. pi = ProcessInfo('Test Process', [ '/bin/echo', 'foo' ])
  72. pi.spawn()
  73. # wait for old process to work...
  74. self.assertEqual(os.read(self.pipes[0], 100), b"foo\n")
  75. # respawn it
  76. old_pid = pi.pid
  77. pi.respawn()
  78. os.dup2(self.old_stdout, sys.stdout.fileno())
  79. # make sure the new one started properly
  80. self.assertEqual(pi.name, 'Test Process')
  81. self.assertEqual(pi.args, [ '/bin/echo', 'foo' ])
  82. # self.assertEqual(pi.env, { 'PATH': os.environ['PATH'],
  83. # 'PYTHON_EXEC': os.environ['PYTHON_EXEC'] })
  84. self.assertEqual(pi.dev_null_stdout, False)
  85. self.assertEqual(os.read(self.pipes[0], 100), b"foo\n")
  86. self.assertNotEqual(pi.process, None)
  87. self.assertTrue(type(pi.pid) is int)
  88. self.assertNotEqual(pi.pid, old_pid)
  89. class TestBoB(unittest.TestCase):
  90. def test_init(self):
  91. bob = BoB()
  92. self.assertEqual(bob.verbose, False)
  93. self.assertEqual(bob.msgq_socket_file, None)
  94. self.assertEqual(bob.cc_session, None)
  95. self.assertEqual(bob.ccs, None)
  96. self.assertEqual(bob.processes, {})
  97. self.assertEqual(bob.dead_processes, {})
  98. self.assertEqual(bob.runnable, False)
  99. self.assertEqual(bob.uid, None)
  100. self.assertEqual(bob.username, None)
  101. self.assertEqual(bob.nocache, False)
  102. def test_init_alternate_socket(self):
  103. bob = BoB("alt_socket_file")
  104. self.assertEqual(bob.verbose, False)
  105. self.assertEqual(bob.msgq_socket_file, "alt_socket_file")
  106. self.assertEqual(bob.cc_session, None)
  107. self.assertEqual(bob.ccs, None)
  108. self.assertEqual(bob.processes, {})
  109. self.assertEqual(bob.dead_processes, {})
  110. self.assertEqual(bob.runnable, False)
  111. self.assertEqual(bob.uid, None)
  112. self.assertEqual(bob.username, None)
  113. self.assertEqual(bob.nocache, False)
  114. def test_command_handler(self):
  115. class DummySession():
  116. def group_sendmsg(self, msg, group):
  117. (self.msg, self.group) = (msg, group)
  118. def group_recvmsg(self, nonblock, seq): pass
  119. bob = BoB()
  120. bob.verbose = True
  121. bob.cc_session = DummySession()
  122. # a bad command
  123. self.assertEqual(bob.command_handler(-1, None),
  124. isc.config.ccsession.create_answer(1, "bad command"))
  125. # "shutdown" command
  126. self.assertEqual(bob.command_handler("shutdown", None),
  127. isc.config.ccsession.create_answer(0))
  128. self.assertFalse(bob.runnable)
  129. # "getstats" command
  130. self.assertEqual(bob.command_handler("getstats", None),
  131. isc.config.ccsession.create_answer(0,
  132. { "stats_data": {
  133. 'bind10.boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', _BASETIME)
  134. }}))
  135. # "sendstats" command
  136. self.assertEqual(bob.command_handler("sendstats", None),
  137. isc.config.ccsession.create_answer(0))
  138. self.assertEqual(bob.cc_session.group, "Stats")
  139. self.assertEqual(bob.cc_session.msg,
  140. isc.config.ccsession.create_command(
  141. 'set', { "stats_data": {
  142. 'bind10.boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', _BASETIME)
  143. }}))
  144. # "ping" command
  145. self.assertEqual(bob.command_handler("ping", None),
  146. isc.config.ccsession.create_answer(0, "pong"))
  147. # "show_processes" command
  148. self.assertEqual(bob.command_handler("show_processes", None),
  149. isc.config.ccsession.create_answer(0,
  150. bob.get_processes()))
  151. # an unknown command
  152. self.assertEqual(bob.command_handler("__UNKNOWN__", None),
  153. isc.config.ccsession.create_answer(1, "Unknown command"))
  154. class MockComponent:
  155. def __init__(self, name, pid):
  156. self.name = lambda: name
  157. self.pid = lambda: pid
  158. class MockConfigurator:
  159. def startup(self, config):
  160. pass
  161. def shutdown(self):
  162. pass
  163. def reconfigure(self, config):
  164. pass
  165. # Class for testing the BoB without actually starting processes.
  166. # This is used for testing the start/stop components routines and
  167. # the BoB commands.
  168. #
  169. # Testing that external processes start is outside the scope
  170. # of the unit test, by overriding the process start methods we can check
  171. # that the right processes are started depending on the configuration
  172. # options.
  173. class MockBob(BoB):
  174. def __init__(self):
  175. BoB.__init__(self)
  176. # Set flags as to which of the overridden methods has been run.
  177. self.msgq = False
  178. self.cfgmgr = False
  179. self.ccsession = False
  180. self.auth = False
  181. self.resolver = False
  182. self.xfrout = False
  183. self.xfrin = False
  184. self.zonemgr = False
  185. self.stats = False
  186. self.stats_httpd = False
  187. self.cmdctl = False
  188. self.c_channel_env = {}
  189. self.processes = {
  190. 1: MockComponent('first', 1),
  191. 2: MockComponent('second', 2)
  192. }
  193. self._component_configurator = MockConfigurator()
  194. def read_bind10_config(self):
  195. # Configuration options are set directly
  196. pass
  197. def start_ccsession(self, c_channel_env):
  198. pass
  199. class TestBossComponents(unittest.TestCase):
  200. """
  201. Test the boss propagates component configuration properly to the
  202. component configurator and acts sane.
  203. """
  204. def setUp(self):
  205. self.__param = None
  206. self.__called = False
  207. self.__compconfig = {
  208. 'comp': {
  209. 'kind': 'needed',
  210. 'process': 'cat'
  211. }
  212. }
  213. def __unary_hook(self, param):
  214. """
  215. A hook function that stores the parameter for later examination.
  216. """
  217. self.__param = param
  218. def __nullary_hook(self):
  219. """
  220. A hook function that notes down it was called.
  221. """
  222. self.__called = True
  223. def __check_core(self, config):
  224. """
  225. A function checking that the config contains parts for the valid
  226. core component configuration.
  227. """
  228. self.assertIsNotNone(config)
  229. for component in ['sockcreator', 'msgq', 'cfgmgr']:
  230. self.assertTrue(component in config)
  231. self.assertEqual(component, config[component]['special'])
  232. self.assertEqual('core', config[component]['kind'])
  233. def __check_extended(self, config):
  234. """
  235. This checks that the config contains the core and one more component.
  236. """
  237. self.__check_core(config)
  238. self.assertTrue('comp' in config)
  239. self.assertEqual('cat', config['comp']['process'])
  240. self.assertEqual('needed', config['comp']['kind'])
  241. self.assertEqual(4, len(config))
  242. def test_correct_run(self):
  243. """
  244. Test the situation when we run in usual scenario, nothing fails,
  245. we just start, reconfigure and then stop peacefully.
  246. """
  247. bob = MockBob()
  248. # Start it
  249. orig = bob._component_configurator.startup
  250. bob._component_configurator.startup = self.__unary_hook
  251. bob.start_all_processes()
  252. bob._component_configurator.startup = orig
  253. self.__check_core(self.__param)
  254. self.assertEqual(3, len(self.__param))
  255. # Reconfigure it
  256. self.__param = None
  257. orig = bob._component_configurator.reconfigure
  258. bob._component_configurator.reconfigure = self.__unary_hook
  259. # Otherwise it does not work
  260. bob.runnable = True
  261. bob.config_handler({'components': self.__compconfig})
  262. self.__check_extended(self.__param)
  263. currconfig = self.__param
  264. # If we reconfigure it, but it does not contain the components part,
  265. # nothing is called
  266. bob.config_handler({})
  267. self.assertEqual(self.__param, currconfig)
  268. self.__param = None
  269. bob._component_configurator.reconfigure = orig
  270. # Check a configuration that messes up the core components is rejected.
  271. compconf = dict(self.__compconfig)
  272. compconf['msgq'] = { 'process': 'echo' }
  273. # Is it OK to raise, or should it catch also and convert to error
  274. # answer?
  275. self.assertRaises(Exception, bob.config_handler,
  276. {'components': compconf})
  277. # Stop it
  278. orig = bob._component_configurator.shutdown
  279. bob._component_configurator.shutdown = self.__nullary_hook
  280. self.__called = False
  281. # We can't call shutdown, that one relies on the stuff in main
  282. bob.stop_all_processes()
  283. bob._component_configurator.shutdown = orig
  284. self.assertTrue(self.__called)
  285. def test_init_config(self):
  286. """
  287. Test initial configuration is loaded.
  288. """
  289. bob = MockBob()
  290. # Start it
  291. bob._component_configurator.reconfigure = self.__unary_hook
  292. # We need to return the original read_bind10_config
  293. bob.read_bind10_config = lambda: BoB.read_bind10_config(bob)
  294. # And provide a session to read the data from
  295. class CC:
  296. pass
  297. bob.ccs = CC()
  298. bob.ccs.get_full_config = lambda: {'components': self.__compconfig}
  299. bob.start_all_processes()
  300. self.__check_extended(self.__param)
  301. def test_shutdown_no_pid(self):
  302. """
  303. Test the boss doesn't fail when ve don't have a PID of a component
  304. (which means it's not running).
  305. """
  306. bob = MockBob()
  307. class NoComponent:
  308. def pid(self):
  309. return None
  310. bob.processes = {}
  311. bob.register_process(1, NoComponent())
  312. bob.shutdown()
  313. class TestBossCmd(unittest.TestCase):
  314. def test_ping(self):
  315. """
  316. Confirm simple ping command works.
  317. """
  318. bob = MockBob()
  319. answer = bob.command_handler("ping", None)
  320. self.assertEqual(answer, {'result': [0, 'pong']})
  321. def test_show_processes(self):
  322. """
  323. Confirm getting a list of processes works.
  324. """
  325. bob = MockBob()
  326. bob.processes = {}
  327. answer = bob.command_handler("show_processes", None)
  328. self.assertEqual(answer, {'result': [0, []]})
  329. def test_show_processes_started(self):
  330. """
  331. Confirm getting a list of processes works.
  332. """
  333. bob = MockBob()
  334. bob.start_all_processes()
  335. answer = bob.command_handler("show_processes", None)
  336. processes = [[1, 'first'],
  337. [2, 'second']]
  338. self.assertEqual(answer, {'result': [0, processes]})
  339. class TestParseArgs(unittest.TestCase):
  340. """
  341. This tests parsing of arguments of the bind10 master process.
  342. """
  343. #TODO: Write tests for the original parsing, bad options, etc.
  344. def test_no_opts(self):
  345. """
  346. Test correct default values when no options are passed.
  347. """
  348. options = parse_args([], TestOptParser)
  349. self.assertEqual(None, options.data_path)
  350. self.assertEqual(None, options.config_file)
  351. self.assertEqual(None, options.cmdctl_port)
  352. def test_data_path(self):
  353. """
  354. Test it can parse the data path.
  355. """
  356. self.assertRaises(OptsError, parse_args, ['-p'], TestOptParser)
  357. self.assertRaises(OptsError, parse_args, ['--data-path'],
  358. TestOptParser)
  359. options = parse_args(['-p', '/data/path'], TestOptParser)
  360. self.assertEqual('/data/path', options.data_path)
  361. options = parse_args(['--data-path=/data/path'], TestOptParser)
  362. self.assertEqual('/data/path', options.data_path)
  363. def test_config_filename(self):
  364. """
  365. Test it can parse the config switch.
  366. """
  367. self.assertRaises(OptsError, parse_args, ['-c'], TestOptParser)
  368. self.assertRaises(OptsError, parse_args, ['--config-file'],
  369. TestOptParser)
  370. options = parse_args(['-c', 'config-file'], TestOptParser)
  371. self.assertEqual('config-file', options.config_file)
  372. options = parse_args(['--config-file=config-file'], TestOptParser)
  373. self.assertEqual('config-file', options.config_file)
  374. def test_cmdctl_port(self):
  375. """
  376. Test it can parse the command control port.
  377. """
  378. self.assertRaises(OptsError, parse_args, ['--cmdctl-port=abc'],
  379. TestOptParser)
  380. self.assertRaises(OptsError, parse_args, ['--cmdctl-port=100000000'],
  381. TestOptParser)
  382. self.assertRaises(OptsError, parse_args, ['--cmdctl-port'],
  383. TestOptParser)
  384. options = parse_args(['--cmdctl-port=1234'], TestOptParser)
  385. self.assertEqual(1234, options.cmdctl_port)
  386. def test_brittle(self):
  387. """
  388. Test we can use the "brittle" flag.
  389. """
  390. options = parse_args([], TestOptParser)
  391. self.assertFalse(options.brittle)
  392. options = parse_args(['--brittle'], TestOptParser)
  393. self.assertTrue(options.brittle)
  394. class TestPIDFile(unittest.TestCase):
  395. def setUp(self):
  396. self.pid_file = '@builddir@' + os.sep + 'bind10.pid'
  397. if os.path.exists(self.pid_file):
  398. os.unlink(self.pid_file)
  399. def tearDown(self):
  400. if os.path.exists(self.pid_file):
  401. os.unlink(self.pid_file)
  402. def check_pid_file(self):
  403. # dump PID to the file, and confirm the content is correct
  404. dump_pid(self.pid_file)
  405. my_pid = os.getpid()
  406. self.assertEqual(my_pid, int(open(self.pid_file, "r").read()))
  407. def test_dump_pid(self):
  408. self.check_pid_file()
  409. # make sure any existing content will be removed
  410. open(self.pid_file, "w").write('dummy data\n')
  411. self.check_pid_file()
  412. def test_unlink_pid_file_notexist(self):
  413. dummy_data = 'dummy_data\n'
  414. open(self.pid_file, "w").write(dummy_data)
  415. unlink_pid_file("no_such_pid_file")
  416. # the file specified for unlink_pid_file doesn't exist,
  417. # and the original content of the file should be intact.
  418. self.assertEqual(dummy_data, open(self.pid_file, "r").read())
  419. def test_dump_pid_with_none(self):
  420. # Check the behavior of dump_pid() and unlink_pid_file() with None.
  421. # This should be no-op.
  422. dump_pid(None)
  423. self.assertFalse(os.path.exists(self.pid_file))
  424. dummy_data = 'dummy_data\n'
  425. open(self.pid_file, "w").write(dummy_data)
  426. unlink_pid_file(None)
  427. self.assertEqual(dummy_data, open(self.pid_file, "r").read())
  428. def test_dump_pid_failure(self):
  429. # the attempt to open file will fail, which should result in exception.
  430. self.assertRaises(IOError, dump_pid,
  431. 'nonexistent_dir' + os.sep + 'bind10.pid')
  432. # TODO: Do we want brittle mode? Probably yes. So we need to re-enable to after that.
  433. @unittest.skip("Brittle mode temporarily broken")
  434. class TestBrittle(unittest.TestCase):
  435. def test_brittle_disabled(self):
  436. bob = MockBob()
  437. bob.start_all_processes()
  438. bob.runnable = True
  439. bob.reap_children()
  440. self.assertTrue(bob.runnable)
  441. def simulated_exit(self):
  442. ret_val = self.exit_info
  443. self.exit_info = (0, 0)
  444. return ret_val
  445. def test_brittle_enabled(self):
  446. bob = MockBob()
  447. bob.start_all_processes()
  448. bob.runnable = True
  449. bob.brittle = True
  450. self.exit_info = (5, 0)
  451. bob._get_process_exit_status = self.simulated_exit
  452. old_stdout = sys.stdout
  453. sys.stdout = open("/dev/null", "w")
  454. bob.reap_children()
  455. sys.stdout = old_stdout
  456. self.assertFalse(bob.runnable)
  457. if __name__ == '__main__':
  458. isc.log.resetUnitTestRootLogger()
  459. unittest.main()