bind10_test.py.in 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200
  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. import bind10_src
  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 copy
  23. import signal
  24. import socket
  25. from isc.net.addr import IPAddr
  26. import time
  27. import isc
  28. import isc.log
  29. import isc.bind10.socket_cache
  30. from isc.testutils.parse_args import TestOptParser, OptsError
  31. class TestProcessInfo(unittest.TestCase):
  32. def setUp(self):
  33. # redirect stdout to a pipe so we can check that our
  34. # process spawning is doing the right thing with stdout
  35. self.old_stdout = os.dup(sys.stdout.fileno())
  36. self.pipes = os.pipe()
  37. os.dup2(self.pipes[1], sys.stdout.fileno())
  38. os.close(self.pipes[1])
  39. # note that we use dup2() to restore the original stdout
  40. # to the main program ASAP in each test... this prevents
  41. # hangs reading from the child process (as the pipe is only
  42. # open in the child), and also insures nice pretty output
  43. def tearDown(self):
  44. # clean up our stdout munging
  45. os.dup2(self.old_stdout, sys.stdout.fileno())
  46. os.close(self.pipes[0])
  47. def test_init(self):
  48. pi = ProcessInfo('Test Process', [ '/bin/echo', 'foo' ])
  49. pi.spawn()
  50. os.dup2(self.old_stdout, sys.stdout.fileno())
  51. self.assertEqual(pi.name, 'Test Process')
  52. self.assertEqual(pi.args, [ '/bin/echo', 'foo' ])
  53. # self.assertEqual(pi.env, { 'PATH': os.environ['PATH'],
  54. # 'PYTHON_EXEC': os.environ['PYTHON_EXEC'] })
  55. self.assertEqual(pi.dev_null_stdout, False)
  56. self.assertEqual(os.read(self.pipes[0], 100), b"foo\n")
  57. self.assertNotEqual(pi.process, None)
  58. self.assertTrue(type(pi.pid) is int)
  59. # def test_setting_env(self):
  60. # pi = ProcessInfo('Test Process', [ '/bin/true' ], env={'FOO': 'BAR'})
  61. # os.dup2(self.old_stdout, sys.stdout.fileno())
  62. # self.assertEqual(pi.env, { 'PATH': os.environ['PATH'],
  63. # 'PYTHON_EXEC': os.environ['PYTHON_EXEC'],
  64. # 'FOO': 'BAR' })
  65. def test_setting_null_stdout(self):
  66. pi = ProcessInfo('Test Process', [ '/bin/echo', 'foo' ],
  67. dev_null_stdout=True)
  68. pi.spawn()
  69. os.dup2(self.old_stdout, sys.stdout.fileno())
  70. self.assertEqual(pi.dev_null_stdout, True)
  71. self.assertEqual(os.read(self.pipes[0], 100), b"")
  72. def test_respawn(self):
  73. pi = ProcessInfo('Test Process', [ '/bin/echo', 'foo' ])
  74. pi.spawn()
  75. # wait for old process to work...
  76. self.assertEqual(os.read(self.pipes[0], 100), b"foo\n")
  77. # respawn it
  78. old_pid = pi.pid
  79. pi.respawn()
  80. os.dup2(self.old_stdout, sys.stdout.fileno())
  81. # make sure the new one started properly
  82. self.assertEqual(pi.name, 'Test Process')
  83. self.assertEqual(pi.args, [ '/bin/echo', 'foo' ])
  84. # self.assertEqual(pi.env, { 'PATH': os.environ['PATH'],
  85. # 'PYTHON_EXEC': os.environ['PYTHON_EXEC'] })
  86. self.assertEqual(pi.dev_null_stdout, False)
  87. self.assertEqual(os.read(self.pipes[0], 100), b"foo\n")
  88. self.assertNotEqual(pi.process, None)
  89. self.assertTrue(type(pi.pid) is int)
  90. self.assertNotEqual(pi.pid, old_pid)
  91. class TestCacheCommands(unittest.TestCase):
  92. """
  93. Test methods of boss related to the socket cache and socket handling.
  94. """
  95. def setUp(self):
  96. """
  97. Prepare the boss for some tests.
  98. Also prepare some variables we need.
  99. """
  100. self.__boss = BoB()
  101. # Fake the cache here so we can pretend it is us and hijack the
  102. # calls to its methods.
  103. self.__boss._socket_cache = self
  104. self.__boss._socket_path = '/socket/path'
  105. self.__raise_exception = None
  106. self.__socket_args = {
  107. "port": 53,
  108. "address": "::",
  109. "protocol": "UDP",
  110. "share_mode": "ANY",
  111. "share_name": "app"
  112. }
  113. # What was and wasn't called.
  114. self.__drop_app_called = None
  115. self.__get_socket_called = None
  116. self.__send_fd_called = None
  117. self.__get_token_called = None
  118. self.__drop_socket_called = None
  119. bind10_src.libutil_io_python.send_fd = self.__send_fd
  120. def __send_fd(self, to, socket):
  121. """
  122. A function to hook the send_fd in the bind10_src.
  123. """
  124. self.__send_fd_called = (to, socket)
  125. class FalseSocket:
  126. """
  127. A socket where we can fake methods we need instead of having a real
  128. socket.
  129. """
  130. def __init__(self):
  131. self.send = ""
  132. def fileno(self):
  133. """
  134. The file number. Used for identifying the remote application.
  135. """
  136. return 42
  137. def sendall(self, data):
  138. """
  139. Adds data to the self.send.
  140. """
  141. self.send += data
  142. def drop_application(self, application):
  143. """
  144. Part of pretending to be the cache. Logs the parameter to
  145. self.__drop_app_called.
  146. In the case self.__raise_exception is set, the exception there
  147. is raised instead.
  148. """
  149. if self.__raise_exception is not None:
  150. raise self.__raise_exception
  151. self.__drop_app_called = application
  152. def test_consumer_dead(self):
  153. """
  154. Test that it calls the drop_application method of the cache.
  155. """
  156. self.__boss.socket_consumer_dead(self.FalseSocket())
  157. self.assertEqual(42, self.__drop_app_called)
  158. def test_consumer_dead_invalid(self):
  159. """
  160. Test that it doesn't crash in case the application is not known to
  161. the cache, the boss doesn't crash, as this actually can happen in
  162. practice.
  163. """
  164. self.__raise_exception = ValueError("This application is unknown")
  165. # This doesn't crash
  166. self.__boss.socket_consumer_dead(self.FalseSocket())
  167. def get_socket(self, token, application):
  168. """
  169. Part of pretending to be the cache. If there's anything in
  170. __raise_exception, it is raised. Otherwise, the call is logged
  171. into __get_socket_called and a number is returned.
  172. """
  173. if self.__raise_exception is not None:
  174. raise self.__raise_exception
  175. self.__get_socket_called = (token, application)
  176. return 13
  177. def test_request_handler(self):
  178. """
  179. Test that a request for socket is forwarded and the socket is sent
  180. back, if it returns a socket.
  181. """
  182. socket = self.FalseSocket()
  183. # An exception from the cache
  184. self.__raise_exception = ValueError("Test value error")
  185. self.__boss.socket_request_handler("token", socket)
  186. # It was called, but it threw, so it is not noted here
  187. self.assertIsNone(self.__get_socket_called)
  188. self.assertEqual("0\n", socket.send)
  189. # It should not have sent any socket.
  190. self.assertIsNone(self.__send_fd_called)
  191. # Now prepare a valid scenario
  192. self.__raise_exception = None
  193. socket.send = ""
  194. self.__boss.socket_request_handler("token", socket)
  195. self.assertEqual("1\n", socket.send)
  196. self.assertEqual((42, 13), self.__send_fd_called)
  197. self.assertEqual(("token", 42), self.__get_socket_called)
  198. def get_token(self, protocol, address, port, share_mode, share_name):
  199. """
  200. Part of pretending to be the cache. If there's anything in
  201. __raise_exception, it is raised. Otherwise, the parameters are
  202. logged into __get_token_called and a token is returned.
  203. """
  204. if self.__raise_exception is not None:
  205. raise self.__raise_exception
  206. self.__get_token_called = (protocol, address, port, share_mode,
  207. share_name)
  208. return "token"
  209. def test_get_socket_ok(self):
  210. """
  211. Test the successful scenario of getting a socket.
  212. """
  213. result = self.__boss._get_socket(self.__socket_args)
  214. [code, answer] = result['result']
  215. self.assertEqual(0, code)
  216. self.assertEqual({
  217. 'token': 'token',
  218. 'path': '/socket/path'
  219. }, answer)
  220. addr = self.__get_token_called[1]
  221. self.assertTrue(isinstance(addr, IPAddr))
  222. self.assertEqual("::", str(addr))
  223. self.assertEqual(("UDP", addr, 53, "ANY", "app"),
  224. self.__get_token_called)
  225. def test_get_socket_error(self):
  226. """
  227. Test that bad inputs are handled correctly, etc.
  228. """
  229. def check_code(code, args):
  230. """
  231. Pass the args there and check if it returns success or not.
  232. The rest is not tested, as it is already checked in the
  233. test_get_socket_ok.
  234. """
  235. [rcode, ranswer] = self.__boss._get_socket(args)['result']
  236. self.assertEqual(code, rcode)
  237. if code == 1:
  238. # This should be an error message. The exact formatting
  239. # is unknown, but we check it is string at least
  240. self.assertTrue(isinstance(ranswer, str))
  241. def mod_args(name, value):
  242. """
  243. Override a parameter in the args.
  244. """
  245. result = dict(self.__socket_args)
  246. result[name] = value
  247. return result
  248. # Port too large
  249. check_code(1, mod_args('port', 65536))
  250. # Not numeric address
  251. check_code(1, mod_args('address', 'example.org.'))
  252. # Some bad values of enum-like params
  253. check_code(1, mod_args('protocol', 'BAD PROTO'))
  254. check_code(1, mod_args('share_mode', 'BAD SHARE'))
  255. # Check missing parameters
  256. for param in self.__socket_args.keys():
  257. args = dict(self.__socket_args)
  258. del args[param]
  259. check_code(1, args)
  260. # These are OK values for the enum-like parameters
  261. # The ones from test_get_socket_ok are not tested here
  262. check_code(0, mod_args('protocol', 'TCP'))
  263. check_code(0, mod_args('share_mode', 'SAMEAPP'))
  264. check_code(0, mod_args('share_mode', 'NO'))
  265. # If an exception is raised from within the cache, it is converted
  266. # to an error, not propagated
  267. self.__raise_exception = Exception("Test exception")
  268. check_code(1, self.__socket_args)
  269. def drop_socket(self, token):
  270. """
  271. Part of pretending to be the cache. If there's anything in
  272. __raise_exception, it is raised. Otherwise, the parameter is stored
  273. in __drop_socket_called.
  274. """
  275. if self.__raise_exception is not None:
  276. raise self.__raise_exception
  277. self.__drop_socket_called = token
  278. def test_drop_socket(self):
  279. """
  280. Check the drop_socket command. It should directly call the method
  281. on the cache. Exceptions should be translated to error messages.
  282. """
  283. # This should be OK and just propagated to the call.
  284. self.assertEqual({"result": [0]},
  285. self.__boss.command_handler("drop_socket",
  286. {"token": "token"}))
  287. self.assertEqual("token", self.__drop_socket_called)
  288. self.__drop_socket_called = None
  289. # Missing parameter
  290. self.assertEqual({"result": [1, "Missing token parameter"]},
  291. self.__boss.command_handler("drop_socket", {}))
  292. self.assertIsNone(self.__drop_socket_called)
  293. # An exception is raised from within the cache
  294. self.__raise_exception = ValueError("Test error")
  295. self.assertEqual({"result": [1, "Test error"]},
  296. self.__boss.command_handler("drop_socket",
  297. {"token": "token"}))
  298. class TestBoB(unittest.TestCase):
  299. def test_init(self):
  300. bob = BoB()
  301. self.assertEqual(bob.verbose, False)
  302. self.assertEqual(bob.msgq_socket_file, None)
  303. self.assertEqual(bob.cc_session, None)
  304. self.assertEqual(bob.ccs, None)
  305. self.assertEqual(bob.components, {})
  306. self.assertEqual(bob.runnable, False)
  307. self.assertEqual(bob.uid, None)
  308. self.assertEqual(bob.username, None)
  309. self.assertEqual(bob.nocache, False)
  310. self.assertIsNone(bob._socket_cache)
  311. def test_set_creator(self):
  312. """
  313. Test the call to set_creator. First time, the cache is created
  314. with the passed creator. The next time, it throws an exception.
  315. """
  316. bob = BoB()
  317. # The cache doesn't use it at start, so just create an empty class
  318. class Creator: pass
  319. creator = Creator()
  320. bob.set_creator(creator)
  321. self.assertTrue(isinstance(bob._socket_cache,
  322. isc.bind10.socket_cache.Cache))
  323. self.assertEqual(creator, bob._socket_cache._creator)
  324. self.assertRaises(ValueError, bob.set_creator, creator)
  325. def test_init_alternate_socket(self):
  326. bob = BoB("alt_socket_file")
  327. self.assertEqual(bob.verbose, False)
  328. self.assertEqual(bob.msgq_socket_file, "alt_socket_file")
  329. self.assertEqual(bob.cc_session, None)
  330. self.assertEqual(bob.ccs, None)
  331. self.assertEqual(bob.components, {})
  332. self.assertEqual(bob.runnable, False)
  333. self.assertEqual(bob.uid, None)
  334. self.assertEqual(bob.username, None)
  335. self.assertEqual(bob.nocache, False)
  336. def test_command_handler(self):
  337. class DummySession():
  338. def group_sendmsg(self, msg, group):
  339. (self.msg, self.group) = (msg, group)
  340. def group_recvmsg(self, nonblock, seq): pass
  341. class DummyModuleCCSession():
  342. module_spec = isc.config.module_spec.ModuleSpec({
  343. "module_name": "Boss",
  344. "statistics": [
  345. {
  346. "item_name": "boot_time",
  347. "item_type": "string",
  348. "item_optional": False,
  349. "item_default": "1970-01-01T00:00:00Z",
  350. "item_title": "Boot time",
  351. "item_description": "A date time when bind10 process starts initially",
  352. "item_format": "date-time"
  353. }
  354. ]
  355. })
  356. def get_module_spec(self):
  357. return self.module_spec
  358. bob = BoB()
  359. bob.verbose = True
  360. bob.cc_session = DummySession()
  361. bob.ccs = DummyModuleCCSession()
  362. # a bad command
  363. self.assertEqual(bob.command_handler(-1, None),
  364. isc.config.ccsession.create_answer(1, "bad command"))
  365. # "shutdown" command
  366. self.assertEqual(bob.command_handler("shutdown", None),
  367. isc.config.ccsession.create_answer(0))
  368. self.assertFalse(bob.runnable)
  369. # "getstats" command
  370. self.assertEqual(bob.command_handler("getstats", None),
  371. isc.config.ccsession.create_answer(0,
  372. { "owner": "Boss",
  373. "data": {
  374. 'boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', _BASETIME)
  375. }}))
  376. # "sendstats" command
  377. self.assertEqual(bob.command_handler("sendstats", None),
  378. isc.config.ccsession.create_answer(0))
  379. self.assertEqual(bob.cc_session.group, "Stats")
  380. self.assertEqual(bob.cc_session.msg,
  381. isc.config.ccsession.create_command(
  382. "set", { "owner": "Boss",
  383. "data": {
  384. "boot_time": time.strftime("%Y-%m-%dT%H:%M:%SZ", _BASETIME)
  385. }}))
  386. # "ping" command
  387. self.assertEqual(bob.command_handler("ping", None),
  388. isc.config.ccsession.create_answer(0, "pong"))
  389. # "show_processes" command
  390. self.assertEqual(bob.command_handler("show_processes", None),
  391. isc.config.ccsession.create_answer(0,
  392. bob.get_processes()))
  393. # an unknown command
  394. self.assertEqual(bob.command_handler("__UNKNOWN__", None),
  395. isc.config.ccsession.create_answer(1, "Unknown command"))
  396. # Fake the _get_socket, which is complicated and tested elsewhere
  397. # We just want to pass the parameters in and let it create a response
  398. def get_socket(args):
  399. return isc.config.ccsession.create_answer(0, args)
  400. bob._get_socket = get_socket
  401. args = {
  402. "port": 53,
  403. "address": "0.0.0.0",
  404. "protocol": "UDP",
  405. "share_mode": "ANY",
  406. "share_name": "app"
  407. }
  408. # Test it just returns whatever it got. The real function doesn't
  409. # work like this, but we don't want the command_handler to touch it
  410. # at all and this is the easiest way to check.
  411. self.assertEqual({'result': [0, args]},
  412. bob.command_handler("get_socket", args))
  413. # The drop_socket is not tested here, but in TestCacheCommands.
  414. # It needs the cache mocks to be in place and they are there.
  415. # Class for testing the BoB without actually starting processes.
  416. # This is used for testing the start/stop components routines and
  417. # the BoB commands.
  418. #
  419. # Testing that external processes start is outside the scope
  420. # of the unit test, by overriding the process start methods we can check
  421. # that the right processes are started depending on the configuration
  422. # options.
  423. class MockBob(BoB):
  424. def __init__(self):
  425. BoB.__init__(self)
  426. # Set flags as to which of the overridden methods has been run.
  427. self.msgq = False
  428. self.cfgmgr = False
  429. self.ccsession = False
  430. self.auth = False
  431. self.resolver = False
  432. self.xfrout = False
  433. self.xfrin = False
  434. self.zonemgr = False
  435. self.stats = False
  436. self.stats_httpd = False
  437. self.cmdctl = False
  438. self.dhcp6 = False
  439. self.dhcp4 = False
  440. self.c_channel_env = {}
  441. self.components = { }
  442. self.creator = False
  443. class MockSockCreator(isc.bind10.component.Component):
  444. def __init__(self, process, boss, kind, address=None, params=None):
  445. isc.bind10.component.Component.__init__(self, process, boss,
  446. kind, 'SockCreator')
  447. self._start_func = boss.start_creator
  448. specials = isc.bind10.special_component.get_specials()
  449. specials['sockcreator'] = MockSockCreator
  450. self._component_configurator = \
  451. isc.bind10.component.Configurator(self, specials)
  452. def start_creator(self):
  453. self.creator = True
  454. procinfo = ProcessInfo('b10-sockcreator', ['/bin/false'])
  455. procinfo.pid = 1
  456. return procinfo
  457. def _read_bind10_config(self):
  458. # Configuration options are set directly
  459. pass
  460. def start_msgq(self):
  461. self.msgq = True
  462. procinfo = ProcessInfo('b10-msgq', ['/bin/false'])
  463. procinfo.pid = 2
  464. return procinfo
  465. def start_ccsession(self, c_channel_env):
  466. # this is not a process, don't have to do anything with procinfo
  467. self.ccsession = True
  468. def start_cfgmgr(self):
  469. self.cfgmgr = True
  470. procinfo = ProcessInfo('b10-cfgmgr', ['/bin/false'])
  471. procinfo.pid = 3
  472. return procinfo
  473. def start_auth(self):
  474. self.auth = True
  475. procinfo = ProcessInfo('b10-auth', ['/bin/false'])
  476. procinfo.pid = 5
  477. return procinfo
  478. def start_resolver(self):
  479. self.resolver = True
  480. procinfo = ProcessInfo('b10-resolver', ['/bin/false'])
  481. procinfo.pid = 6
  482. return procinfo
  483. def start_simple(self, name):
  484. procmap = { 'b10-zonemgr': self.start_zonemgr,
  485. 'b10-stats': self.start_stats,
  486. 'b10-stats-httpd': self.start_stats_httpd,
  487. 'b10-cmdctl': self.start_cmdctl,
  488. 'b10-dhcp6': self.start_dhcp6,
  489. 'b10-dhcp4': self.start_dhcp4 }
  490. return procmap[name]()
  491. def start_xfrout(self):
  492. self.xfrout = True
  493. procinfo = ProcessInfo('b10-xfrout', ['/bin/false'])
  494. procinfo.pid = 7
  495. return procinfo
  496. def start_xfrin(self):
  497. self.xfrin = True
  498. procinfo = ProcessInfo('b10-xfrin', ['/bin/false'])
  499. procinfo.pid = 8
  500. return procinfo
  501. def start_zonemgr(self):
  502. self.zonemgr = True
  503. procinfo = ProcessInfo('b10-zonemgr', ['/bin/false'])
  504. procinfo.pid = 9
  505. return procinfo
  506. def start_stats(self):
  507. self.stats = True
  508. procinfo = ProcessInfo('b10-stats', ['/bin/false'])
  509. procinfo.pid = 10
  510. return procinfo
  511. def start_stats_httpd(self):
  512. self.stats_httpd = True
  513. procinfo = ProcessInfo('b10-stats-httpd', ['/bin/false'])
  514. procinfo.pid = 11
  515. return procinfo
  516. def start_cmdctl(self):
  517. self.cmdctl = True
  518. procinfo = ProcessInfo('b10-cmdctl', ['/bin/false'])
  519. procinfo.pid = 12
  520. return procinfo
  521. def start_dhcp6(self):
  522. self.dhcp6 = True
  523. procinfo = ProcessInfo('b10-dhcp6', ['/bin/false'])
  524. procinfo.pid = 13
  525. return procinfo
  526. def start_dhcp4(self):
  527. self.dhcp4 = True
  528. procinfo = ProcessInfo('b10-dhcp4', ['/bin/false'])
  529. procinfo.pid = 14
  530. return procinfo
  531. def stop_process(self, process, recipient):
  532. procmap = { 'b10-auth': self.stop_auth,
  533. 'b10-resolver': self.stop_resolver,
  534. 'b10-xfrout': self.stop_xfrout,
  535. 'b10-xfrin': self.stop_xfrin,
  536. 'b10-zonemgr': self.stop_zonemgr,
  537. 'b10-stats': self.stop_stats,
  538. 'b10-stats-httpd': self.stop_stats_httpd,
  539. 'b10-cmdctl': self.stop_cmdctl }
  540. procmap[process]()
  541. # Some functions to pretend we stop processes, use by stop_process
  542. def stop_msgq(self):
  543. if self.msgq:
  544. del self.components[2]
  545. self.msgq = False
  546. def stop_cfgmgr(self):
  547. if self.cfgmgr:
  548. del self.components[3]
  549. self.cfgmgr = False
  550. def stop_auth(self):
  551. if self.auth:
  552. del self.components[5]
  553. self.auth = False
  554. def stop_resolver(self):
  555. if self.resolver:
  556. del self.components[6]
  557. self.resolver = False
  558. def stop_xfrout(self):
  559. if self.xfrout:
  560. del self.components[7]
  561. self.xfrout = False
  562. def stop_xfrin(self):
  563. if self.xfrin:
  564. del self.components[8]
  565. self.xfrin = False
  566. def stop_zonemgr(self):
  567. if self.zonemgr:
  568. del self.components[9]
  569. self.zonemgr = False
  570. def stop_stats(self):
  571. if self.stats:
  572. del self.components[10]
  573. self.stats = False
  574. def stop_stats_httpd(self):
  575. if self.stats_httpd:
  576. del self.components[11]
  577. self.stats_httpd = False
  578. def stop_cmdctl(self):
  579. if self.cmdctl:
  580. del self.components[12]
  581. self.cmdctl = False
  582. class TestStartStopProcessesBob(unittest.TestCase):
  583. """
  584. Check that the start_all_components method starts the right combination
  585. of components and that the right components are started and stopped
  586. according to changes in configuration.
  587. """
  588. def check_environment_unchanged(self):
  589. # Check whether the environment has not been changed
  590. self.assertEqual(original_os_environ, os.environ)
  591. def check_started(self, bob, core, auth, resolver):
  592. """
  593. Check that the right sets of services are started. The ones that
  594. should be running are specified by the core, auth and resolver parameters
  595. (they are groups of processes, eg. auth means b10-auth, -xfrout, -xfrin
  596. and -zonemgr).
  597. """
  598. self.assertEqual(bob.msgq, core)
  599. self.assertEqual(bob.cfgmgr, core)
  600. self.assertEqual(bob.ccsession, core)
  601. self.assertEqual(bob.creator, core)
  602. self.assertEqual(bob.auth, auth)
  603. self.assertEqual(bob.resolver, resolver)
  604. self.assertEqual(bob.xfrout, auth)
  605. self.assertEqual(bob.xfrin, auth)
  606. self.assertEqual(bob.zonemgr, auth)
  607. self.assertEqual(bob.stats, core)
  608. self.assertEqual(bob.stats_httpd, core)
  609. self.assertEqual(bob.cmdctl, core)
  610. self.check_environment_unchanged()
  611. def check_preconditions(self, bob):
  612. self.check_started(bob, False, False, False)
  613. def check_started_none(self, bob):
  614. """
  615. Check that the situation is according to configuration where no servers
  616. should be started. Some components still need to be running.
  617. """
  618. self.check_started(bob, True, False, False)
  619. self.check_environment_unchanged()
  620. def check_started_both(self, bob):
  621. """
  622. Check the situation is according to configuration where both servers
  623. (auth and resolver) are enabled.
  624. """
  625. self.check_started(bob, True, True, True)
  626. self.check_environment_unchanged()
  627. def check_started_auth(self, bob):
  628. """
  629. Check the set of components needed to run auth only is started.
  630. """
  631. self.check_started(bob, True, True, False)
  632. self.check_environment_unchanged()
  633. def check_started_resolver(self, bob):
  634. """
  635. Check the set of components needed to run resolver only is started.
  636. """
  637. self.check_started(bob, True, False, True)
  638. self.check_environment_unchanged()
  639. def check_started_dhcp(self, bob, v4, v6):
  640. """
  641. Check if proper combinations of DHCPv4 and DHCpv6 can be started
  642. """
  643. self.assertEqual(v4, bob.dhcp4)
  644. self.assertEqual(v6, bob.dhcp6)
  645. self.check_environment_unchanged()
  646. def construct_config(self, start_auth, start_resolver):
  647. # The things that are common, not turned on an off
  648. config = {}
  649. config['b10-stats'] = { 'kind': 'dispensable', 'address': 'Stats' }
  650. config['b10-stats-httpd'] = { 'kind': 'dispensable',
  651. 'address': 'StatsHttpd' }
  652. config['b10-cmdctl'] = { 'kind': 'needed', 'special': 'cmdctl' }
  653. if start_auth:
  654. config['b10-auth'] = { 'kind': 'needed', 'special': 'auth' }
  655. config['b10-xfrout'] = { 'kind': 'dispensable',
  656. 'special': 'xfrout' }
  657. config['b10-xfrin'] = { 'kind': 'dispensable', 'special': 'xfrin' }
  658. config['b10-zonemgr'] = { 'kind': 'dispensable',
  659. 'address': 'Zonemgr' }
  660. if start_resolver:
  661. config['b10-resolver'] = { 'kind': 'needed',
  662. 'special': 'resolver' }
  663. return {'components': config}
  664. def config_start_init(self, start_auth, start_resolver):
  665. """
  666. Test the configuration is loaded at the startup.
  667. """
  668. bob = MockBob()
  669. config = self.construct_config(start_auth, start_resolver)
  670. class CC:
  671. def get_full_config(self):
  672. return config
  673. # Provide the fake CC with data
  674. bob.ccs = CC()
  675. # And make sure it's not overwritten
  676. def start_ccsession():
  677. bob.ccsession = True
  678. bob.start_ccsession = lambda _: start_ccsession()
  679. # We need to return the original _read_bind10_config
  680. bob._read_bind10_config = lambda: BoB._read_bind10_config(bob)
  681. bob.start_all_components()
  682. self.check_started(bob, True, start_auth, start_resolver)
  683. self.check_environment_unchanged()
  684. def test_start_none(self):
  685. self.config_start_init(False, False)
  686. def test_start_resolver(self):
  687. self.config_start_init(False, True)
  688. def test_start_auth(self):
  689. self.config_start_init(True, False)
  690. def test_start_both(self):
  691. self.config_start_init(True, True)
  692. def test_config_start(self):
  693. """
  694. Test that the configuration starts and stops components according
  695. to configuration changes.
  696. """
  697. # Create BoB and ensure correct initialization
  698. bob = MockBob()
  699. self.check_preconditions(bob)
  700. bob.start_all_components()
  701. bob.runnable = True
  702. bob.config_handler(self.construct_config(False, False))
  703. self.check_started_none(bob)
  704. # Enable both at once
  705. bob.config_handler(self.construct_config(True, True))
  706. self.check_started_both(bob)
  707. # Not touched by empty change
  708. bob.config_handler({})
  709. self.check_started_both(bob)
  710. # Not touched by change to the same configuration
  711. bob.config_handler(self.construct_config(True, True))
  712. self.check_started_both(bob)
  713. # Turn them both off again
  714. bob.config_handler(self.construct_config(False, False))
  715. self.check_started_none(bob)
  716. # Not touched by empty change
  717. bob.config_handler({})
  718. self.check_started_none(bob)
  719. # Not touched by change to the same configuration
  720. bob.config_handler(self.construct_config(False, False))
  721. self.check_started_none(bob)
  722. # Start and stop auth separately
  723. bob.config_handler(self.construct_config(True, False))
  724. self.check_started_auth(bob)
  725. bob.config_handler(self.construct_config(False, False))
  726. self.check_started_none(bob)
  727. # Start and stop resolver separately
  728. bob.config_handler(self.construct_config(False, True))
  729. self.check_started_resolver(bob)
  730. bob.config_handler(self.construct_config(False, False))
  731. self.check_started_none(bob)
  732. # Alternate
  733. bob.config_handler(self.construct_config(True, False))
  734. self.check_started_auth(bob)
  735. bob.config_handler(self.construct_config(False, True))
  736. self.check_started_resolver(bob)
  737. bob.config_handler(self.construct_config(True, False))
  738. self.check_started_auth(bob)
  739. def test_config_start_once(self):
  740. """
  741. Tests that a component is started only once.
  742. """
  743. # Create BoB and ensure correct initialization
  744. bob = MockBob()
  745. self.check_preconditions(bob)
  746. bob.start_all_components()
  747. bob.runnable = True
  748. bob.config_handler(self.construct_config(True, True))
  749. self.check_started_both(bob)
  750. bob.start_auth = lambda: self.fail("Started auth again")
  751. bob.start_xfrout = lambda: self.fail("Started xfrout again")
  752. bob.start_xfrin = lambda: self.fail("Started xfrin again")
  753. bob.start_zonemgr = lambda: self.fail("Started zonemgr again")
  754. bob.start_resolver = lambda: self.fail("Started resolver again")
  755. # Send again we want to start them. Should not do it, as they are.
  756. bob.config_handler(self.construct_config(True, True))
  757. def test_config_not_started_early(self):
  758. """
  759. Test that components are not started by the config handler before
  760. startup.
  761. """
  762. bob = MockBob()
  763. self.check_preconditions(bob)
  764. bob.start_auth = lambda: self.fail("Started auth again")
  765. bob.start_xfrout = lambda: self.fail("Started xfrout again")
  766. bob.start_xfrin = lambda: self.fail("Started xfrin again")
  767. bob.start_zonemgr = lambda: self.fail("Started zonemgr again")
  768. bob.start_resolver = lambda: self.fail("Started resolver again")
  769. bob.config_handler({'start_auth': True, 'start_resolver': True})
  770. # Checks that DHCP (v4 and v6) components are started when expected
  771. def test_start_dhcp(self):
  772. # Create BoB and ensure correct initialization
  773. bob = MockBob()
  774. self.check_preconditions(bob)
  775. bob.start_all_components()
  776. bob.config_handler(self.construct_config(False, False))
  777. self.check_started_dhcp(bob, False, False)
  778. def test_start_dhcp_v6only(self):
  779. # Create BoB and ensure correct initialization
  780. bob = MockBob()
  781. self.check_preconditions(bob)
  782. # v6 only enabled
  783. bob.start_all_components()
  784. bob.runnable = True
  785. bob._BoB_started = True
  786. config = self.construct_config(False, False)
  787. config['components']['b10-dhcp6'] = { 'kind': 'needed',
  788. 'address': 'Dhcp6' }
  789. bob.config_handler(config)
  790. self.check_started_dhcp(bob, False, True)
  791. # uncomment when dhcpv4 becomes implemented
  792. # v4 only enabled
  793. #bob.cfg_start_dhcp6 = False
  794. #bob.cfg_start_dhcp4 = True
  795. #self.check_started_dhcp(bob, True, False)
  796. # both v4 and v6 enabled
  797. #bob.cfg_start_dhcp6 = True
  798. #bob.cfg_start_dhcp4 = True
  799. #self.check_started_dhcp(bob, True, True)
  800. class MockComponent:
  801. def __init__(self, name, pid):
  802. self.name = lambda: name
  803. self.pid = lambda: pid
  804. class TestBossCmd(unittest.TestCase):
  805. def test_ping(self):
  806. """
  807. Confirm simple ping command works.
  808. """
  809. bob = MockBob()
  810. answer = bob.command_handler("ping", None)
  811. self.assertEqual(answer, {'result': [0, 'pong']})
  812. def test_show_processes_empty(self):
  813. """
  814. Confirm getting a list of processes works.
  815. """
  816. bob = MockBob()
  817. answer = bob.command_handler("show_processes", None)
  818. self.assertEqual(answer, {'result': [0, []]})
  819. def test_show_processes(self):
  820. """
  821. Confirm getting a list of processes works.
  822. """
  823. bob = MockBob()
  824. bob.register_process(1, MockComponent('first', 1))
  825. bob.register_process(2, MockComponent('second', 2))
  826. answer = bob.command_handler("show_processes", None)
  827. processes = [[1, 'first'],
  828. [2, 'second']]
  829. self.assertEqual(answer, {'result': [0, processes]})
  830. class TestParseArgs(unittest.TestCase):
  831. """
  832. This tests parsing of arguments of the bind10 master process.
  833. """
  834. #TODO: Write tests for the original parsing, bad options, etc.
  835. def test_no_opts(self):
  836. """
  837. Test correct default values when no options are passed.
  838. """
  839. options = parse_args([], TestOptParser)
  840. self.assertEqual(None, options.data_path)
  841. self.assertEqual(None, options.config_file)
  842. self.assertEqual(None, options.cmdctl_port)
  843. def test_data_path(self):
  844. """
  845. Test it can parse the data path.
  846. """
  847. self.assertRaises(OptsError, parse_args, ['-p'], TestOptParser)
  848. self.assertRaises(OptsError, parse_args, ['--data-path'],
  849. TestOptParser)
  850. options = parse_args(['-p', '/data/path'], TestOptParser)
  851. self.assertEqual('/data/path', options.data_path)
  852. options = parse_args(['--data-path=/data/path'], TestOptParser)
  853. self.assertEqual('/data/path', options.data_path)
  854. def test_config_filename(self):
  855. """
  856. Test it can parse the config switch.
  857. """
  858. self.assertRaises(OptsError, parse_args, ['-c'], TestOptParser)
  859. self.assertRaises(OptsError, parse_args, ['--config-file'],
  860. TestOptParser)
  861. options = parse_args(['-c', 'config-file'], TestOptParser)
  862. self.assertEqual('config-file', options.config_file)
  863. options = parse_args(['--config-file=config-file'], TestOptParser)
  864. self.assertEqual('config-file', options.config_file)
  865. def test_cmdctl_port(self):
  866. """
  867. Test it can parse the command control port.
  868. """
  869. self.assertRaises(OptsError, parse_args, ['--cmdctl-port=abc'],
  870. TestOptParser)
  871. self.assertRaises(OptsError, parse_args, ['--cmdctl-port=100000000'],
  872. TestOptParser)
  873. self.assertRaises(OptsError, parse_args, ['--cmdctl-port'],
  874. TestOptParser)
  875. options = parse_args(['--cmdctl-port=1234'], TestOptParser)
  876. self.assertEqual(1234, options.cmdctl_port)
  877. class TestPIDFile(unittest.TestCase):
  878. def setUp(self):
  879. self.pid_file = '@builddir@' + os.sep + 'bind10.pid'
  880. if os.path.exists(self.pid_file):
  881. os.unlink(self.pid_file)
  882. def tearDown(self):
  883. if os.path.exists(self.pid_file):
  884. os.unlink(self.pid_file)
  885. def check_pid_file(self):
  886. # dump PID to the file, and confirm the content is correct
  887. dump_pid(self.pid_file)
  888. my_pid = os.getpid()
  889. self.assertEqual(my_pid, int(open(self.pid_file, "r").read()))
  890. def test_dump_pid(self):
  891. self.check_pid_file()
  892. # make sure any existing content will be removed
  893. open(self.pid_file, "w").write('dummy data\n')
  894. self.check_pid_file()
  895. def test_unlink_pid_file_notexist(self):
  896. dummy_data = 'dummy_data\n'
  897. open(self.pid_file, "w").write(dummy_data)
  898. unlink_pid_file("no_such_pid_file")
  899. # the file specified for unlink_pid_file doesn't exist,
  900. # and the original content of the file should be intact.
  901. self.assertEqual(dummy_data, open(self.pid_file, "r").read())
  902. def test_dump_pid_with_none(self):
  903. # Check the behavior of dump_pid() and unlink_pid_file() with None.
  904. # This should be no-op.
  905. dump_pid(None)
  906. self.assertFalse(os.path.exists(self.pid_file))
  907. dummy_data = 'dummy_data\n'
  908. open(self.pid_file, "w").write(dummy_data)
  909. unlink_pid_file(None)
  910. self.assertEqual(dummy_data, open(self.pid_file, "r").read())
  911. def test_dump_pid_failure(self):
  912. # the attempt to open file will fail, which should result in exception.
  913. self.assertRaises(IOError, dump_pid,
  914. 'nonexistent_dir' + os.sep + 'bind10.pid')
  915. class TestBossComponents(unittest.TestCase):
  916. """
  917. Test the boss propagates component configuration properly to the
  918. component configurator and acts sane.
  919. """
  920. def setUp(self):
  921. self.__param = None
  922. self.__called = False
  923. self.__compconfig = {
  924. 'comp': {
  925. 'kind': 'needed',
  926. 'process': 'cat'
  927. }
  928. }
  929. def __unary_hook(self, param):
  930. """
  931. A hook function that stores the parameter for later examination.
  932. """
  933. self.__param = param
  934. def __nullary_hook(self):
  935. """
  936. A hook function that notes down it was called.
  937. """
  938. self.__called = True
  939. def __check_core(self, config):
  940. """
  941. A function checking that the config contains parts for the valid
  942. core component configuration.
  943. """
  944. self.assertIsNotNone(config)
  945. for component in ['sockcreator', 'msgq', 'cfgmgr']:
  946. self.assertTrue(component in config)
  947. self.assertEqual(component, config[component]['special'])
  948. self.assertEqual('core', config[component]['kind'])
  949. def __check_extended(self, config):
  950. """
  951. This checks that the config contains the core and one more component.
  952. """
  953. self.__check_core(config)
  954. self.assertTrue('comp' in config)
  955. self.assertEqual('cat', config['comp']['process'])
  956. self.assertEqual('needed', config['comp']['kind'])
  957. self.assertEqual(4, len(config))
  958. def test_correct_run(self):
  959. """
  960. Test the situation when we run in usual scenario, nothing fails,
  961. we just start, reconfigure and then stop peacefully.
  962. """
  963. bob = MockBob()
  964. # Start it
  965. orig = bob._component_configurator.startup
  966. bob._component_configurator.startup = self.__unary_hook
  967. bob.start_all_components()
  968. bob._component_configurator.startup = orig
  969. self.__check_core(self.__param)
  970. self.assertEqual(3, len(self.__param))
  971. # Reconfigure it
  972. self.__param = None
  973. orig = bob._component_configurator.reconfigure
  974. bob._component_configurator.reconfigure = self.__unary_hook
  975. # Otherwise it does not work
  976. bob.runnable = True
  977. bob.config_handler({'components': self.__compconfig})
  978. self.__check_extended(self.__param)
  979. currconfig = self.__param
  980. # If we reconfigure it, but it does not contain the components part,
  981. # nothing is called
  982. bob.config_handler({})
  983. self.assertEqual(self.__param, currconfig)
  984. self.__param = None
  985. bob._component_configurator.reconfigure = orig
  986. # Check a configuration that messes up the core components is rejected.
  987. compconf = dict(self.__compconfig)
  988. compconf['msgq'] = { 'process': 'echo' }
  989. result = bob.config_handler({'components': compconf})
  990. # Check it rejected it
  991. self.assertEqual(1, result['result'][0])
  992. # We can't call shutdown, that one relies on the stuff in main
  993. # We check somewhere else that the shutdown is actually called
  994. # from there (the test_kills).
  995. def test_kills(self):
  996. """
  997. Test that the boss kills components which don't want to stop.
  998. """
  999. bob = MockBob()
  1000. killed = []
  1001. class ImmortalComponent:
  1002. """
  1003. An immortal component. It does not stop when it is told so
  1004. (anyway it is not told so). It does not die if it is killed
  1005. the first time. It dies only when killed forcefully.
  1006. """
  1007. def kill(self, forcefull=False):
  1008. killed.append(forcefull)
  1009. if forcefull:
  1010. bob.components = {}
  1011. def pid(self):
  1012. return 1
  1013. def name(self):
  1014. return "Immortal"
  1015. bob.components = {}
  1016. bob.register_process(1, ImmortalComponent())
  1017. # While at it, we check the configurator shutdown is actually called
  1018. orig = bob._component_configurator.shutdown
  1019. bob._component_configurator.shutdown = self.__nullary_hook
  1020. self.__called = False
  1021. bob.shutdown()
  1022. self.assertEqual([False, True], killed)
  1023. self.assertTrue(self.__called)
  1024. bob._component_configurator.shutdown = orig
  1025. def test_component_shutdown(self):
  1026. """
  1027. Test the component_shutdown sets all variables accordingly.
  1028. """
  1029. bob = MockBob()
  1030. self.assertRaises(Exception, bob.component_shutdown, 1)
  1031. self.assertEqual(1, bob.exitcode)
  1032. bob._BoB__started = True
  1033. bob.component_shutdown(2)
  1034. self.assertEqual(2, bob.exitcode)
  1035. self.assertFalse(bob.runnable)
  1036. def test_init_config(self):
  1037. """
  1038. Test initial configuration is loaded.
  1039. """
  1040. bob = MockBob()
  1041. # Start it
  1042. bob._component_configurator.reconfigure = self.__unary_hook
  1043. # We need to return the original read_bind10_config
  1044. bob._read_bind10_config = lambda: BoB._read_bind10_config(bob)
  1045. # And provide a session to read the data from
  1046. class CC:
  1047. pass
  1048. bob.ccs = CC()
  1049. bob.ccs.get_full_config = lambda: {'components': self.__compconfig}
  1050. bob.start_all_components()
  1051. self.__check_extended(self.__param)
  1052. if __name__ == '__main__':
  1053. # store os.environ for test_unchanged_environment
  1054. original_os_environ = copy.deepcopy(os.environ)
  1055. isc.log.resetUnitTestRootLogger()
  1056. unittest.main()