test_utils.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  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, session=None, nonblock=False, timeout=None):
  47. if session is not None:
  48. cc_session = session
  49. else:
  50. cc_session = isc.cc.Session()
  51. if timeout is not None:
  52. orig_timeout = cc_session.get_timeout()
  53. cc_session.set_timeout(timeout * 1000)
  54. command = isc.config.ccsession.create_command(command_name, params)
  55. seq = cc_session.group_sendmsg(command, module_name)
  56. try:
  57. (answer, env) = cc_session.group_recvmsg(nonblock, seq)
  58. if answer:
  59. return isc.config.ccsession.parse_answer(answer)
  60. except isc.cc.SessionTimeout:
  61. pass
  62. finally:
  63. if timeout is not None:
  64. cc_session.set_timeout(orig_timeout)
  65. if session is None:
  66. cc_session.close()
  67. def send_shutdown(module_name, **kwargs):
  68. return send_command("shutdown", module_name, **kwargs)
  69. class ThreadingServerManager:
  70. def __init__(self, server, *args, **kwargs):
  71. self.server = server(*args, **kwargs)
  72. self.server_name = server.__name__
  73. self.server._thread = threading.Thread(
  74. name=self.server_name, target=self.server.run)
  75. self.server._thread.daemon = True
  76. def run(self):
  77. self.server._thread.start()
  78. self.server._started.wait()
  79. self.server._started.clear()
  80. def shutdown(self, blocking=False):
  81. """Shut down the server by calling its own shutdown() method.
  82. Then wait for its thread to finish. If blocking is True,
  83. the thread.join() blocks until the thread finishes. If not,
  84. it uses a zero timeout. The latter is necessary in a number
  85. of existing tests. We should redo this part (we should not
  86. even need threads in most, if not all, of these threads, see
  87. ticket #1668)"""
  88. self.server.shutdown()
  89. if blocking:
  90. self.server._thread.join()
  91. else:
  92. self.server._thread.join(0) # timeout is 0
  93. class MockMsgq:
  94. def __init__(self):
  95. self._started = threading.Event()
  96. self.msgq = msgq.MsgQ(verbose=False)
  97. result = self.msgq.setup()
  98. if result:
  99. sys.exit("Error on Msgq startup: %s" % result)
  100. def run(self):
  101. self._started.set()
  102. try:
  103. self.msgq.run()
  104. finally:
  105. # Make sure all the sockets, etc, are removed once it stops.
  106. self.msgq.shutdown()
  107. def shutdown(self):
  108. # Ask it to terminate nicely
  109. self.msgq.stop()
  110. class MockCfgmgr:
  111. def __init__(self):
  112. self._started = threading.Event()
  113. self.cfgmgr = isc.config.cfgmgr.ConfigManager(
  114. os.environ['CONFIG_TESTDATA_PATH'], "b10-config.db")
  115. self.cfgmgr.read_config()
  116. def run(self):
  117. self._started.set()
  118. try:
  119. self.cfgmgr.run()
  120. except Exception:
  121. pass
  122. def shutdown(self):
  123. self.cfgmgr.running = False
  124. class MockInit:
  125. spec_str = """\
  126. {
  127. "module_spec": {
  128. "module_name": "Init",
  129. "module_description": "Mock Master process",
  130. "config_data": [
  131. {
  132. "item_name": "components",
  133. "item_type": "named_set",
  134. "item_optional": false,
  135. "item_default": {
  136. "b10-stats": { "address": "Stats", "kind": "dispensable" },
  137. "b10-cmdctl": { "special": "cmdctl", "kind": "needed" }
  138. },
  139. "named_set_item_spec": {
  140. "item_name": "component",
  141. "item_type": "map",
  142. "item_optional": false,
  143. "item_default": { },
  144. "map_item_spec": [
  145. {
  146. "item_name": "special",
  147. "item_optional": true,
  148. "item_type": "string"
  149. },
  150. {
  151. "item_name": "process",
  152. "item_optional": true,
  153. "item_type": "string"
  154. },
  155. {
  156. "item_name": "kind",
  157. "item_optional": false,
  158. "item_type": "string",
  159. "item_default": "dispensable"
  160. },
  161. {
  162. "item_name": "address",
  163. "item_optional": true,
  164. "item_type": "string"
  165. },
  166. {
  167. "item_name": "params",
  168. "item_optional": true,
  169. "item_type": "list",
  170. "list_item_spec": {
  171. "item_name": "param",
  172. "item_optional": false,
  173. "item_type": "string",
  174. "item_default": ""
  175. }
  176. },
  177. {
  178. "item_name": "priority",
  179. "item_optional": true,
  180. "item_type": "integer"
  181. }
  182. ]
  183. }
  184. }
  185. ],
  186. "commands": [
  187. {
  188. "command_name": "shutdown",
  189. "command_description": "Shut down BIND 10",
  190. "command_args": []
  191. },
  192. {
  193. "command_name": "ping",
  194. "command_description": "Ping the b10-init process",
  195. "command_args": []
  196. },
  197. {
  198. "command_name": "show_processes",
  199. "command_description": "List the running BIND 10 processes",
  200. "command_args": []
  201. }
  202. ],
  203. "statistics": [
  204. {
  205. "item_name": "boot_time",
  206. "item_type": "string",
  207. "item_optional": false,
  208. "item_default": "1970-01-01T00:00:00Z",
  209. "item_title": "Boot time",
  210. "item_description": "A date time when bind10 process starts initially",
  211. "item_format": "date-time"
  212. }
  213. ]
  214. }
  215. }
  216. """
  217. _BASETIME = CONST_BASETIME
  218. def __init__(self):
  219. self._started = threading.Event()
  220. self.running = False
  221. self.spec_file = io.StringIO(self.spec_str)
  222. # create ModuleCCSession object
  223. self.mccs = isc.config.ModuleCCSession(
  224. self.spec_file,
  225. self.config_handler,
  226. self.command_handler)
  227. self.spec_file.close()
  228. self.cc_session = self.mccs._session
  229. self.got_command_name = ''
  230. self.pid_list = [[ 9999, "b10-auth", "Auth" ],
  231. [ 9998, "b10-auth-2", "Auth" ]]
  232. self.statistics_data = {
  233. 'boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', self._BASETIME)
  234. }
  235. def run(self):
  236. self.mccs.start()
  237. self.running = True
  238. self._started.set()
  239. try:
  240. while self.running:
  241. self.mccs.check_command(False)
  242. except Exception:
  243. pass
  244. def shutdown(self):
  245. self.running = False
  246. def config_handler(self, new_config):
  247. return isc.config.create_answer(0)
  248. def command_handler(self, command, *args, **kwargs):
  249. self._started.set()
  250. self.got_command_name = command
  251. sdata = self.statistics_data
  252. if command == 'getstats':
  253. return isc.config.create_answer(0, sdata)
  254. elif command == 'show_processes':
  255. # Return dummy pids
  256. return isc.config.create_answer(
  257. 0, self.pid_list)
  258. return isc.config.create_answer(1, "Unknown Command")
  259. class MockAuth:
  260. spec_str = """\
  261. {
  262. "module_spec": {
  263. "module_name": "Auth",
  264. "module_description": "Mock Authoritative service",
  265. "config_data": [],
  266. "commands": [],
  267. "statistics": [
  268. {
  269. "item_name": "queries.tcp",
  270. "item_type": "integer",
  271. "item_optional": false,
  272. "item_default": 0,
  273. "item_title": "Queries TCP",
  274. "item_description": "A number of total query counts which all auth servers receive over TCP since they started initially"
  275. },
  276. {
  277. "item_name": "queries.udp",
  278. "item_type": "integer",
  279. "item_optional": false,
  280. "item_default": 0,
  281. "item_title": "Queries UDP",
  282. "item_description": "A number of total query counts which all auth servers receive over UDP since they started initially"
  283. },
  284. {
  285. "item_name": "queries.perzone",
  286. "item_type": "list",
  287. "item_optional": false,
  288. "item_default": [
  289. {
  290. "zonename" : "test1.example",
  291. "queries.udp" : 1,
  292. "queries.tcp" : 2
  293. },
  294. {
  295. "zonename" : "test2.example",
  296. "queries.udp" : 3,
  297. "queries.tcp" : 4
  298. }
  299. ],
  300. "item_title": "Queries per zone",
  301. "item_description": "Queries per zone",
  302. "list_item_spec": {
  303. "item_name": "zones",
  304. "item_type": "map",
  305. "item_optional": false,
  306. "item_default": {},
  307. "map_item_spec": [
  308. {
  309. "item_name": "zonename",
  310. "item_type": "string",
  311. "item_optional": false,
  312. "item_default": "",
  313. "item_title": "Zonename",
  314. "item_description": "Zonename"
  315. },
  316. {
  317. "item_name": "queries.udp",
  318. "item_type": "integer",
  319. "item_optional": false,
  320. "item_default": 0,
  321. "item_title": "Queries UDP per zone",
  322. "item_description": "A number of UDP query counts per zone"
  323. },
  324. {
  325. "item_name": "queries.tcp",
  326. "item_type": "integer",
  327. "item_optional": false,
  328. "item_default": 0,
  329. "item_title": "Queries TCP per zone",
  330. "item_description": "A number of TCP query counts per zone"
  331. }
  332. ]
  333. }
  334. },
  335. {
  336. "item_name": "nds_queries.perzone",
  337. "item_type": "named_set",
  338. "item_optional": false,
  339. "item_default": {
  340. "test10.example" : {
  341. "queries.udp" : 1,
  342. "queries.tcp" : 2
  343. },
  344. "test20.example" : {
  345. "queries.udp" : 3,
  346. "queries.tcp" : 4
  347. }
  348. },
  349. "item_title": "Queries per zone",
  350. "item_description": "Queries per zone",
  351. "named_set_item_spec": {
  352. "item_name": "zonename",
  353. "item_type": "map",
  354. "item_optional": false,
  355. "item_default": {},
  356. "item_title": "Zonename",
  357. "item_description": "Zonename",
  358. "map_item_spec": [
  359. {
  360. "item_name": "queries.udp",
  361. "item_type": "integer",
  362. "item_optional": false,
  363. "item_default": 0,
  364. "item_title": "Queries UDP per zone",
  365. "item_description": "A number of UDP query counts per zone"
  366. },
  367. {
  368. "item_name": "queries.tcp",
  369. "item_type": "integer",
  370. "item_optional": false,
  371. "item_default": 0,
  372. "item_title": "Queries TCP per zone",
  373. "item_description": "A number of TCP query counts per zone"
  374. }
  375. ]
  376. }
  377. }
  378. ]
  379. }
  380. }
  381. """
  382. def __init__(self):
  383. self._started = threading.Event()
  384. self.running = False
  385. self.spec_file = io.StringIO(self.spec_str)
  386. # create ModuleCCSession object
  387. self.mccs = isc.config.ModuleCCSession(
  388. self.spec_file,
  389. self.config_handler,
  390. self.command_handler)
  391. self.spec_file.close()
  392. self.cc_session = self.mccs._session
  393. self.got_command_name = ''
  394. self.queries_tcp = 3
  395. self.queries_udp = 2
  396. self.queries_per_zone = [{
  397. 'zonename': 'test1.example',
  398. 'queries.tcp': 5,
  399. 'queries.udp': 4
  400. }]
  401. self.nds_queries_per_zone = {
  402. 'test10.example': {
  403. 'queries.tcp': 5,
  404. 'queries.udp': 4
  405. }
  406. }
  407. def run(self):
  408. self.mccs.start()
  409. self.running = True
  410. self._started.set()
  411. try:
  412. while self.running:
  413. self.mccs.check_command(False)
  414. except Exception:
  415. pass
  416. def shutdown(self):
  417. self.running = False
  418. def config_handler(self, new_config):
  419. return isc.config.create_answer(0)
  420. def command_handler(self, command, *args, **kwargs):
  421. self.got_command_name = command
  422. sdata = { 'queries.tcp': self.queries_tcp,
  423. 'queries.udp': self.queries_udp,
  424. 'queries.perzone' : self.queries_per_zone,
  425. 'nds_queries.perzone' : {
  426. 'test10.example': {
  427. 'queries.tcp': \
  428. isc.cc.data.find(
  429. self.nds_queries_per_zone,
  430. 'test10.example/queries.tcp')
  431. }
  432. },
  433. 'nds_queries.perzone/test10.example/queries.udp' :
  434. isc.cc.data.find(self.nds_queries_per_zone,
  435. 'test10.example/queries.udp')
  436. }
  437. if command == 'getstats':
  438. return isc.config.create_answer(0, sdata)
  439. return isc.config.create_answer(1, "Unknown Command")
  440. class MyStats(stats.Stats):
  441. stats._BASETIME = CONST_BASETIME
  442. stats.get_timestamp = lambda: time.mktime(CONST_BASETIME)
  443. stats.get_datetime = lambda x=None: time.strftime("%Y-%m-%dT%H:%M:%SZ", CONST_BASETIME)
  444. def __init__(self):
  445. self._started = threading.Event()
  446. stats.Stats.__init__(self)
  447. def run(self):
  448. self._started.set()
  449. try:
  450. self.start()
  451. except Exception:
  452. pass
  453. def shutdown(self):
  454. self.command_shutdown()
  455. class MyStatsHttpd(stats_httpd.StatsHttpd):
  456. ORIG_SPECFILE_LOCATION = stats_httpd.SPECFILE_LOCATION
  457. def __init__(self, *server_address):
  458. self._started = threading.Event()
  459. if server_address:
  460. stats_httpd.SPECFILE_LOCATION = self.create_specfile(*server_address)
  461. try:
  462. stats_httpd.StatsHttpd.__init__(self)
  463. finally:
  464. if hasattr(stats_httpd.SPECFILE_LOCATION, "close"):
  465. stats_httpd.SPECFILE_LOCATION.close()
  466. stats_httpd.SPECFILE_LOCATION = self.ORIG_SPECFILE_LOCATION
  467. else:
  468. stats_httpd.StatsHttpd.__init__(self)
  469. def create_specfile(self, *server_address):
  470. spec_io = open(self.ORIG_SPECFILE_LOCATION)
  471. try:
  472. spec = json.load(spec_io)
  473. spec_io.close()
  474. config = spec['module_spec']['config_data']
  475. for i in range(len(config)):
  476. if config[i]['item_name'] == 'listen_on':
  477. config[i]['item_default'] = \
  478. [ dict(address=a[0], port=a[1]) for a in server_address ]
  479. break
  480. return io.StringIO(json.dumps(spec))
  481. finally:
  482. spec_io.close()
  483. def run(self):
  484. self._started.set()
  485. try:
  486. self.start()
  487. except Exception:
  488. pass
  489. def shutdown(self):
  490. self.command_handler('shutdown', None)
  491. class BaseModules:
  492. def __init__(self):
  493. # MockMsgq
  494. self.msgq = ThreadingServerManager(MockMsgq)
  495. self.msgq.run()
  496. # Check whether msgq is ready. A SessionTimeout is raised here if not.
  497. isc.cc.session.Session().close()
  498. # MockCfgmgr
  499. self.cfgmgr = ThreadingServerManager(MockCfgmgr)
  500. self.cfgmgr.run()
  501. # MockInit
  502. self.b10_init = ThreadingServerManager(MockInit)
  503. self.b10_init.run()
  504. # MockAuth
  505. self.auth = ThreadingServerManager(MockAuth)
  506. self.auth.run()
  507. self.auth2 = ThreadingServerManager(MockAuth)
  508. self.auth2.run()
  509. def shutdown(self):
  510. # MockMsgq. We need to wait (blocking) for it, otherwise it'll wipe out
  511. # a socket for another test during its shutdown.
  512. self.msgq.shutdown(True)
  513. # We also wait for the others, but these are just so we don't create
  514. # too many threads in parallel.
  515. # MockAuth
  516. self.auth2.shutdown(True)
  517. self.auth.shutdown(True)
  518. # MockInit
  519. self.b10_init.shutdown(True)
  520. # MockCfgmgr
  521. self.cfgmgr.shutdown(True)
  522. # remove the unused socket file
  523. socket_file = self.msgq.server.msgq.socket_file
  524. try:
  525. if os.path.exists(socket_file):
  526. os.remove(socket_file)
  527. except OSError:
  528. pass