test_utils.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. # Copyright (C) 2011-2012 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. """
  16. Utilities and mock modules for unittests of statistics modules
  17. """
  18. import os
  19. import io
  20. import time
  21. import sys
  22. import threading
  23. import tempfile
  24. import json
  25. import signal
  26. import msgq
  27. import isc.config.cfgmgr
  28. import stats
  29. import stats_httpd
  30. CONST_BASETIME = (2011, 6, 22, 8, 14, 8, 2, 173, 0)
  31. class SignalHandler():
  32. """A signal handler class for deadlock in unittest"""
  33. def __init__(self, fail_handler, timeout=20):
  34. """sets a schedule in SIGARM for invoking the handler via
  35. unittest.TestCase after timeout seconds (default is 20)"""
  36. self.fail_handler = fail_handler
  37. self.orig_handler = signal.signal(signal.SIGALRM, self.sig_handler)
  38. signal.alarm(timeout)
  39. def reset(self):
  40. """resets the schedule in SIGALRM"""
  41. signal.alarm(0)
  42. signal.signal(signal.SIGALRM, self.orig_handler)
  43. def sig_handler(self, signal, frame):
  44. """envokes unittest.TestCase.fail as a signal handler"""
  45. self.fail_handler("A deadlock might be detected")
  46. def send_command(command_name, module_name, params=None):
  47. cc_session = isc.cc.Session()
  48. command = isc.config.ccsession.create_command(command_name, params)
  49. seq = cc_session.group_sendmsg(command, module_name)
  50. try:
  51. (answer, env) = cc_session.group_recvmsg(False, seq)
  52. if answer:
  53. return isc.config.ccsession.parse_answer(answer)
  54. except isc.cc.SessionTimeout:
  55. pass
  56. finally:
  57. cc_session.close()
  58. class ThreadingServerManager:
  59. def __init__(self, server, *args, **kwargs):
  60. self.server = server(*args, **kwargs)
  61. self.server_name = server.__name__
  62. self.server._thread = threading.Thread(
  63. name=self.server_name, target=self.server.run)
  64. self.server._thread.daemon = True
  65. def run(self):
  66. self.server._thread.start()
  67. self.server._started.wait()
  68. self.server._started.clear()
  69. def shutdown(self, blocking=False):
  70. """Shut down the server by calling its own shutdown() method.
  71. Then wait for its thread to finish. If blocking is True,
  72. the thread.join() blocks until the thread finishes. If not,
  73. it uses a zero timeout. The latter is necessary in a number
  74. of existing tests. We should redo this part (we should not
  75. even need threads in most, if not all, of these threads, see
  76. ticket #1668)"""
  77. self.server.shutdown()
  78. if blocking:
  79. self.server._thread.join()
  80. else:
  81. self.server._thread.join(0) # timeout is 0
  82. class MockMsgq:
  83. def __init__(self):
  84. self._started = threading.Event()
  85. self.msgq = msgq.MsgQ(verbose=False)
  86. result = self.msgq.setup()
  87. if result:
  88. sys.exit("Error on Msgq startup: %s" % result)
  89. def run(self):
  90. self._started.set()
  91. try:
  92. self.msgq.run()
  93. finally:
  94. # Make sure all the sockets, etc, are removed once it stops.
  95. self.msgq.shutdown()
  96. def shutdown(self):
  97. # Ask it to terminate nicely
  98. self.msgq.stop()
  99. class MockCfgmgr:
  100. def __init__(self):
  101. self._started = threading.Event()
  102. self.cfgmgr = isc.config.cfgmgr.ConfigManager(
  103. os.environ['CONFIG_TESTDATA_PATH'], "b10-config.db")
  104. self.cfgmgr.read_config()
  105. def run(self):
  106. self._started.set()
  107. try:
  108. self.cfgmgr.run()
  109. except Exception:
  110. pass
  111. def shutdown(self):
  112. self.cfgmgr.running = False
  113. class MockInit:
  114. spec_str = """\
  115. {
  116. "module_spec": {
  117. "module_name": "Init",
  118. "module_description": "Mock Master process",
  119. "config_data": [
  120. {
  121. "item_name": "components",
  122. "item_type": "named_set",
  123. "item_optional": false,
  124. "item_default": {
  125. "b10-stats": { "address": "Stats", "kind": "dispensable" },
  126. "b10-cmdctl": { "special": "cmdctl", "kind": "needed" }
  127. },
  128. "named_set_item_spec": {
  129. "item_name": "component",
  130. "item_type": "map",
  131. "item_optional": false,
  132. "item_default": { },
  133. "map_item_spec": [
  134. {
  135. "item_name": "special",
  136. "item_optional": true,
  137. "item_type": "string"
  138. },
  139. {
  140. "item_name": "process",
  141. "item_optional": true,
  142. "item_type": "string"
  143. },
  144. {
  145. "item_name": "kind",
  146. "item_optional": false,
  147. "item_type": "string",
  148. "item_default": "dispensable"
  149. },
  150. {
  151. "item_name": "address",
  152. "item_optional": true,
  153. "item_type": "string"
  154. },
  155. {
  156. "item_name": "params",
  157. "item_optional": true,
  158. "item_type": "list",
  159. "list_item_spec": {
  160. "item_name": "param",
  161. "item_optional": false,
  162. "item_type": "string",
  163. "item_default": ""
  164. }
  165. },
  166. {
  167. "item_name": "priority",
  168. "item_optional": true,
  169. "item_type": "integer"
  170. }
  171. ]
  172. }
  173. }
  174. ],
  175. "commands": [
  176. {
  177. "command_name": "shutdown",
  178. "command_description": "Shut down BIND 10",
  179. "command_args": []
  180. },
  181. {
  182. "command_name": "ping",
  183. "command_description": "Ping the b10-init process",
  184. "command_args": []
  185. },
  186. {
  187. "command_name": "show_processes",
  188. "command_description": "List the running BIND 10 processes",
  189. "command_args": []
  190. }
  191. ],
  192. "statistics": [
  193. {
  194. "item_name": "boot_time",
  195. "item_type": "string",
  196. "item_optional": false,
  197. "item_default": "1970-01-01T00:00:00Z",
  198. "item_title": "Boot time",
  199. "item_description": "A date time when bind10 process starts initially",
  200. "item_format": "date-time"
  201. }
  202. ]
  203. }
  204. }
  205. """
  206. _BASETIME = CONST_BASETIME
  207. def __init__(self):
  208. self._started = threading.Event()
  209. self.running = False
  210. self.spec_file = io.StringIO(self.spec_str)
  211. # create ModuleCCSession object
  212. self.mccs = isc.config.ModuleCCSession(
  213. self.spec_file,
  214. self.config_handler,
  215. self.command_handler)
  216. self.spec_file.close()
  217. self.cc_session = self.mccs._session
  218. self.got_command_name = ''
  219. self.pid_list = [[ 9999, "b10-auth", "Auth" ],
  220. [ 9998, "b10-auth-2", "Auth" ]]
  221. self.statistics_data = {
  222. 'boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', self._BASETIME)
  223. }
  224. def run(self):
  225. self.mccs.start()
  226. self.running = True
  227. self._started.set()
  228. try:
  229. while self.running:
  230. self.mccs.check_command(False)
  231. except Exception:
  232. pass
  233. def shutdown(self):
  234. self.running = False
  235. def config_handler(self, new_config):
  236. return isc.config.create_answer(0)
  237. def command_handler(self, command, *args, **kwargs):
  238. self._started.set()
  239. self.got_command_name = command
  240. sdata = self.statistics_data
  241. if command == 'getstats':
  242. return isc.config.create_answer(0, sdata)
  243. elif command == 'show_processes':
  244. # Return dummy pids
  245. return isc.config.create_answer(
  246. 0, self.pid_list)
  247. return isc.config.create_answer(1, "Unknown Command")
  248. class MockAuth:
  249. spec_str = """\
  250. {
  251. "module_spec": {
  252. "module_name": "Auth",
  253. "module_description": "Mock Authoritative service",
  254. "config_data": [],
  255. "commands": [],
  256. "statistics": [
  257. {
  258. "item_name": "queries.tcp",
  259. "item_type": "integer",
  260. "item_optional": false,
  261. "item_default": 0,
  262. "item_title": "Queries TCP",
  263. "item_description": "A number of total query counts which all auth servers receive over TCP since they started initially"
  264. },
  265. {
  266. "item_name": "queries.udp",
  267. "item_type": "integer",
  268. "item_optional": false,
  269. "item_default": 0,
  270. "item_title": "Queries UDP",
  271. "item_description": "A number of total query counts which all auth servers receive over UDP since they started initially"
  272. },
  273. {
  274. "item_name": "queries.perzone",
  275. "item_type": "list",
  276. "item_optional": false,
  277. "item_default": [
  278. {
  279. "zonename" : "test1.example",
  280. "queries.udp" : 1,
  281. "queries.tcp" : 2
  282. },
  283. {
  284. "zonename" : "test2.example",
  285. "queries.udp" : 3,
  286. "queries.tcp" : 4
  287. }
  288. ],
  289. "item_title": "Queries per zone",
  290. "item_description": "Queries per zone",
  291. "list_item_spec": {
  292. "item_name": "zones",
  293. "item_type": "map",
  294. "item_optional": false,
  295. "item_default": {},
  296. "map_item_spec": [
  297. {
  298. "item_name": "zonename",
  299. "item_type": "string",
  300. "item_optional": false,
  301. "item_default": "",
  302. "item_title": "Zonename",
  303. "item_description": "Zonename"
  304. },
  305. {
  306. "item_name": "queries.udp",
  307. "item_type": "integer",
  308. "item_optional": false,
  309. "item_default": 0,
  310. "item_title": "Queries UDP per zone",
  311. "item_description": "A number of UDP query counts per zone"
  312. },
  313. {
  314. "item_name": "queries.tcp",
  315. "item_type": "integer",
  316. "item_optional": false,
  317. "item_default": 0,
  318. "item_title": "Queries TCP per zone",
  319. "item_description": "A number of TCP query counts per zone"
  320. }
  321. ]
  322. }
  323. },
  324. {
  325. "item_name": "nds_queries.perzone",
  326. "item_type": "named_set",
  327. "item_optional": false,
  328. "item_default": {
  329. "test10.example" : {
  330. "queries.udp" : 1,
  331. "queries.tcp" : 2
  332. },
  333. "test20.example" : {
  334. "queries.udp" : 3,
  335. "queries.tcp" : 4
  336. }
  337. },
  338. "item_title": "Queries per zone",
  339. "item_description": "Queries per zone",
  340. "named_set_item_spec": {
  341. "item_name": "zonename",
  342. "item_type": "map",
  343. "item_optional": false,
  344. "item_default": {},
  345. "item_title": "Zonename",
  346. "item_description": "Zonename",
  347. "map_item_spec": [
  348. {
  349. "item_name": "queries.udp",
  350. "item_type": "integer",
  351. "item_optional": false,
  352. "item_default": 0,
  353. "item_title": "Queries UDP per zone",
  354. "item_description": "A number of UDP query counts per zone"
  355. },
  356. {
  357. "item_name": "queries.tcp",
  358. "item_type": "integer",
  359. "item_optional": false,
  360. "item_default": 0,
  361. "item_title": "Queries TCP per zone",
  362. "item_description": "A number of TCP query counts per zone"
  363. }
  364. ]
  365. }
  366. }
  367. ]
  368. }
  369. }
  370. """
  371. def __init__(self):
  372. self._started = threading.Event()
  373. self.running = False
  374. self.spec_file = io.StringIO(self.spec_str)
  375. # create ModuleCCSession object
  376. self.mccs = isc.config.ModuleCCSession(
  377. self.spec_file,
  378. self.config_handler,
  379. self.command_handler)
  380. self.spec_file.close()
  381. self.cc_session = self.mccs._session
  382. self.got_command_name = ''
  383. self.queries_tcp = 3
  384. self.queries_udp = 2
  385. self.queries_per_zone = [{
  386. 'zonename': 'test1.example',
  387. 'queries.tcp': 5,
  388. 'queries.udp': 4
  389. }]
  390. self.nds_queries_per_zone = {
  391. 'test10.example': {
  392. 'queries.tcp': 5,
  393. 'queries.udp': 4
  394. }
  395. }
  396. def run(self):
  397. self.mccs.start()
  398. self.running = True
  399. self._started.set()
  400. try:
  401. while self.running:
  402. self.mccs.check_command(False)
  403. except Exception:
  404. pass
  405. def shutdown(self):
  406. self.running = False
  407. def config_handler(self, new_config):
  408. return isc.config.create_answer(0)
  409. def command_handler(self, command, *args, **kwargs):
  410. self.got_command_name = command
  411. sdata = { 'queries.tcp': self.queries_tcp,
  412. 'queries.udp': self.queries_udp,
  413. 'queries.perzone' : self.queries_per_zone,
  414. 'nds_queries.perzone' : {
  415. 'test10.example': {
  416. 'queries.tcp': \
  417. isc.cc.data.find(
  418. self.nds_queries_per_zone,
  419. 'test10.example/queries.tcp')
  420. }
  421. },
  422. 'nds_queries.perzone/test10.example/queries.udp' :
  423. isc.cc.data.find(self.nds_queries_per_zone,
  424. 'test10.example/queries.udp')
  425. }
  426. if command == 'getstats':
  427. return isc.config.create_answer(0, sdata)
  428. return isc.config.create_answer(1, "Unknown Command")
  429. class MyModuleCCSession(isc.config.ConfigData):
  430. """Mocked ModuleCCSession class.
  431. This class incorporates the module spec directly from the file,
  432. and works as if the ModuleCCSession class as much as possible
  433. without involving network I/O.
  434. """
  435. def __init__(self, spec_file, config_handler, command_handler):
  436. module_spec = isc.config.module_spec_from_file(spec_file)
  437. isc.config.ConfigData.__init__(self, module_spec)
  438. self._session = self
  439. self.stopped = False
  440. self.lname = 'mock_mod_ccs'
  441. def start(self):
  442. pass
  443. def send_stopping(self):
  444. self.stopped = True # just record it's called to inspect it later
  445. class SimpleStats(stats.Stats):
  446. """A faked Stats class for unit tests.
  447. This class inherits most of the real Stats class, but replace the
  448. ModuleCCSession with a fake one so we can avoid network I/O in tests,
  449. and can also inspect or tweak messages via the session more easily.
  450. This class also maintains some faked module information and statistics
  451. data that can be retrieved from the implementation of the Stats class.
  452. """
  453. def __init__(self):
  454. # First, setup some internal attributes. All of them are essentially
  455. # private (so prefixed with double '_'), but some are defined as if
  456. # "protected" (with a single '_') for the convenient of tests that
  457. # may want to inspect or tweak them.
  458. # initial seq num for faked group_sendmsg, arbitrary choice.
  459. self.__seq = 4200
  460. # if set, use them as faked response to group_recvmsg (see below).
  461. # it's a list of tuples, each of which is of (answer, envelope).
  462. self._answers = []
  463. # the default answer from faked recvmsg if _answers is empty
  464. self.__default_answer = isc.config.ccsession.create_answer(
  465. 0, {'Init':
  466. json.loads(MockInit.spec_str)['module_spec']['statistics'],
  467. 'Auth':
  468. json.loads(MockAuth.spec_str)['module_spec']['statistics']
  469. })
  470. # setup faked auth statistics
  471. self.__init_auth_stat()
  472. # statistics data for faked Init module
  473. self._init_sdata = {
  474. 'boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME)
  475. }
  476. # Incorporate other setups of the real Stats module. We use the faked
  477. # ModuleCCSession to avoid blocking network operation. Note also that
  478. # we replace _init_statistics_data() (see below), so we don't
  479. # initialize statistics data yet.
  480. stats.Stats.__init__(self, MyModuleCCSession)
  481. # replace some (faked) ModuleCCSession methods so we can inspect/fake
  482. # the data exchanged via the CC session, then call
  483. # _init_statistics_data. This will get the Stats module info from
  484. # the file directly and some amount information about the Init and
  485. # Auth modules (hardcoded below).
  486. self.cc_session.group_sendmsg = self.__check_group_sendmsg
  487. self.cc_session.group_recvmsg = self.__check_group_recvmsg
  488. self.cc_session.rpc_call = self.__rpc_call
  489. stats.Stats._init_statistics_data(self)
  490. def __init_auth_stat(self):
  491. self._queries_tcp = 3
  492. self._queries_udp = 2
  493. self.__queries_per_zone = [{
  494. 'zonename': 'test1.example', 'queries.tcp': 5, 'queries.udp': 4
  495. }]
  496. self.__nds_queries_per_zone = \
  497. { 'test10.example': { 'queries.tcp': 5, 'queries.udp': 4 } }
  498. self._auth_sdata = \
  499. { 'queries.tcp': self._queries_tcp,
  500. 'queries.udp': self._queries_udp,
  501. 'queries.perzone' : self.__queries_per_zone,
  502. 'nds_queries.perzone' : {
  503. 'test10.example': {
  504. 'queries.tcp': isc.cc.data.find(
  505. self.__nds_queries_per_zone,
  506. 'test10.example/queries.tcp')
  507. }
  508. },
  509. 'nds_queries.perzone/test10.example/queries.udp' :
  510. isc.cc.data.find(self.__nds_queries_per_zone,
  511. 'test10.example/queries.udp')
  512. }
  513. def _init_statistics_data(self):
  514. # Inherited from real Stats class, just for deferring the
  515. # initialization until we are ready.
  516. pass
  517. def __check_group_sendmsg(self, command, destination, want_answer=False):
  518. """Faked ModuleCCSession.group_sendmsg for tests.
  519. Skipping actual network communication, and just returning an internally
  520. generated sequence number.
  521. """
  522. self.__seq += 1
  523. return self.__seq
  524. def __check_group_recvmsg(self, nonblocking, seq):
  525. """Faked ModuleCCSession.group_recvmsg for tests.
  526. Skipping actual network communication, and returning an internally
  527. prepared answer. sequence number. If faked anser is given in
  528. _answers, use it; otherwise use the default. we don't actually check
  529. the sequence.
  530. """
  531. if len(self._answers) == 0:
  532. return self.__default_answer, {'from': 'no-matter'}
  533. return self._answers.pop(0)
  534. def __rpc_call(self, command, group):
  535. answer, _ = self.__check_group_recvmsg(None, None)
  536. return isc.config.ccsession.parse_answer(answer)[1]
  537. class MyStats(stats.Stats):
  538. stats._BASETIME = CONST_BASETIME
  539. stats.get_timestamp = lambda: time.mktime(CONST_BASETIME)
  540. stats.get_datetime = lambda x=None: time.strftime("%Y-%m-%dT%H:%M:%SZ", CONST_BASETIME)
  541. def __init__(self):
  542. self._started = threading.Event()
  543. stats.Stats.__init__(self)
  544. def run(self):
  545. self._started.set()
  546. try:
  547. self.start()
  548. except Exception:
  549. pass
  550. def shutdown(self):
  551. self.command_shutdown()
  552. class MyStatsHttpd(stats_httpd.StatsHttpd):
  553. ORIG_SPECFILE_LOCATION = stats_httpd.SPECFILE_LOCATION
  554. def __init__(self, *server_address):
  555. self._started = threading.Event()
  556. if server_address:
  557. stats_httpd.SPECFILE_LOCATION = self.create_specfile(*server_address)
  558. try:
  559. stats_httpd.StatsHttpd.__init__(self)
  560. finally:
  561. if hasattr(stats_httpd.SPECFILE_LOCATION, "close"):
  562. stats_httpd.SPECFILE_LOCATION.close()
  563. stats_httpd.SPECFILE_LOCATION = self.ORIG_SPECFILE_LOCATION
  564. else:
  565. stats_httpd.StatsHttpd.__init__(self)
  566. def create_specfile(self, *server_address):
  567. spec_io = open(self.ORIG_SPECFILE_LOCATION)
  568. try:
  569. spec = json.load(spec_io)
  570. spec_io.close()
  571. config = spec['module_spec']['config_data']
  572. for i in range(len(config)):
  573. if config[i]['item_name'] == 'listen_on':
  574. config[i]['item_default'] = \
  575. [ dict(address=a[0], port=a[1]) for a in server_address ]
  576. break
  577. return io.StringIO(json.dumps(spec))
  578. finally:
  579. spec_io.close()
  580. def run(self):
  581. self._started.set()
  582. try:
  583. self.start()
  584. except Exception:
  585. pass
  586. def shutdown(self):
  587. self.command_handler('shutdown', None)
  588. class BaseModules:
  589. def __init__(self):
  590. # MockMsgq
  591. self.msgq = ThreadingServerManager(MockMsgq)
  592. self.msgq.run()
  593. # Check whether msgq is ready. A SessionTimeout is raised here if not.
  594. isc.cc.session.Session().close()
  595. # MockCfgmgr
  596. self.cfgmgr = ThreadingServerManager(MockCfgmgr)
  597. self.cfgmgr.run()
  598. # MockInit
  599. self.b10_init = ThreadingServerManager(MockInit)
  600. self.b10_init.run()
  601. # MockAuth
  602. self.auth = ThreadingServerManager(MockAuth)
  603. self.auth.run()
  604. self.auth2 = ThreadingServerManager(MockAuth)
  605. self.auth2.run()
  606. def shutdown(self):
  607. # MockMsgq. We need to wait (blocking) for it, otherwise it'll wipe out
  608. # a socket for another test during its shutdown.
  609. self.msgq.shutdown(True)
  610. # We also wait for the others, but these are just so we don't create
  611. # too many threads in parallel.
  612. # MockAuth
  613. self.auth2.shutdown(True)
  614. self.auth.shutdown(True)
  615. # MockInit
  616. self.b10_init.shutdown(True)
  617. # MockCfgmgr
  618. self.cfgmgr.shutdown(True)
  619. # remove the unused socket file
  620. socket_file = self.msgq.server.msgq.socket_file
  621. try:
  622. if os.path.exists(socket_file):
  623. os.remove(socket_file)
  624. except OSError:
  625. pass