test_utils.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. """
  2. Utilities and mock modules for unittests of statistics modules
  3. """
  4. import os
  5. import io
  6. import time
  7. import sys
  8. import threading
  9. import tempfile
  10. import json
  11. import signal
  12. import msgq
  13. import isc.config.cfgmgr
  14. import stats
  15. import stats_httpd
  16. class SignalHandler():
  17. """A signal handler class for deadlock in unittest"""
  18. def __init__(self, fail_handler, timeout=20):
  19. """sets a schedule in SIGARM for invoking the handler via
  20. unittest.TestCase after timeout seconds (default is 20)"""
  21. self.fail_handler = fail_handler
  22. self.orig_handler = signal.signal(signal.SIGALRM, self.sig_handler)
  23. signal.alarm(timeout)
  24. def reset(self):
  25. """resets the schedule in SIGALRM"""
  26. signal.alarm(0)
  27. signal.signal(signal.SIGALRM, self.orig_handler)
  28. def sig_handler(self, signal, frame):
  29. """envokes unittest.TestCase.fail as a signal handler"""
  30. self.fail_handler("A deadlock might be detected")
  31. def send_command(command_name, module_name, params=None, session=None, nonblock=False, timeout=None):
  32. if session is not None:
  33. cc_session = session
  34. else:
  35. cc_session = isc.cc.Session()
  36. if timeout is not None:
  37. orig_timeout = cc_session.get_timeout()
  38. cc_session.set_timeout(timeout * 1000)
  39. command = isc.config.ccsession.create_command(command_name, params)
  40. seq = cc_session.group_sendmsg(command, module_name)
  41. try:
  42. (answer, env) = cc_session.group_recvmsg(nonblock, seq)
  43. if answer:
  44. return isc.config.ccsession.parse_answer(answer)
  45. except isc.cc.SessionTimeout:
  46. pass
  47. finally:
  48. if timeout is not None:
  49. cc_session.set_timeout(orig_timeout)
  50. if session is None:
  51. cc_session.close()
  52. def send_shutdown(module_name, **kwargs):
  53. return send_command("shutdown", module_name, **kwargs)
  54. class ThreadingServerManager:
  55. def __init__(self, server, *args, **kwargs):
  56. self.server = server(*args, **kwargs)
  57. self.server_name = server.__name__
  58. self.server._thread = threading.Thread(
  59. name=self.server_name, target=self.server.run)
  60. self.server._thread.daemon = True
  61. def run(self):
  62. self.server._thread.start()
  63. self.server._started.wait()
  64. self.server._started.clear()
  65. def shutdown(self, blocking=False):
  66. """Shut down the server by calling its own shutdown() method.
  67. Then wait for its thread to finish. If blocking is True,
  68. the thread.join() blocks until the thread finishes. If not,
  69. it uses a zero timeout. The latter is necessary in a number
  70. of existing tests. We should redo this part (we should not
  71. even need threads in most, if not all, of these threads, see
  72. ticket #1668)"""
  73. self.server.shutdown()
  74. if blocking:
  75. self.server._thread.join()
  76. else:
  77. self.server._thread.join(0) # timeout is 0
  78. def do_nothing(*args, **kwargs): pass
  79. class dummy_sys:
  80. """Dummy for sys"""
  81. class dummy_io:
  82. write = do_nothing
  83. stdout = stderr = dummy_io()
  84. class MockMsgq:
  85. def __init__(self):
  86. self._started = threading.Event()
  87. # suppress output to stdout and stderr
  88. msgq.sys = dummy_sys()
  89. msgq.print = do_nothing
  90. self.msgq = msgq.MsgQ(verbose=False)
  91. result = self.msgq.setup()
  92. if result:
  93. sys.exit("Error on Msgq startup: %s" % result)
  94. def run(self):
  95. self._started.set()
  96. try:
  97. self.msgq.run()
  98. except Exception:
  99. pass
  100. finally:
  101. # explicitly shut down the socket of the msgq before
  102. # shutting down the msgq
  103. self.msgq.listen_socket.shutdown(msgq.socket.SHUT_RDWR)
  104. self.msgq.shutdown()
  105. def shutdown(self):
  106. # do nothing
  107. pass
  108. class MockCfgmgr:
  109. def __init__(self):
  110. self._started = threading.Event()
  111. self.cfgmgr = isc.config.cfgmgr.ConfigManager(
  112. os.environ['CONFIG_TESTDATA_PATH'], "b10-config_test.db")
  113. self.cfgmgr.read_config()
  114. def run(self):
  115. self._started.set()
  116. try:
  117. self.cfgmgr.run()
  118. except Exception:
  119. pass
  120. def shutdown(self):
  121. self.cfgmgr.running = False
  122. class MockBoss:
  123. spec_str = """\
  124. {
  125. "module_spec": {
  126. "module_name": "Boss",
  127. "module_description": "Mock Master process",
  128. "config_data": [
  129. {
  130. "item_name": "components",
  131. "item_type": "named_set",
  132. "item_optional": false,
  133. "item_default": {
  134. "b10-stats": { "address": "Stats", "kind": "dispensable" },
  135. "b10-cmdctl": { "special": "cmdctl", "kind": "needed" }
  136. },
  137. "named_set_item_spec": {
  138. "item_name": "component",
  139. "item_type": "map",
  140. "item_optional": false,
  141. "item_default": { },
  142. "map_item_spec": [
  143. {
  144. "item_name": "special",
  145. "item_optional": true,
  146. "item_type": "string"
  147. },
  148. {
  149. "item_name": "process",
  150. "item_optional": true,
  151. "item_type": "string"
  152. },
  153. {
  154. "item_name": "kind",
  155. "item_optional": false,
  156. "item_type": "string",
  157. "item_default": "dispensable"
  158. },
  159. {
  160. "item_name": "address",
  161. "item_optional": true,
  162. "item_type": "string"
  163. },
  164. {
  165. "item_name": "params",
  166. "item_optional": true,
  167. "item_type": "list",
  168. "list_item_spec": {
  169. "item_name": "param",
  170. "item_optional": false,
  171. "item_type": "string",
  172. "item_default": ""
  173. }
  174. },
  175. {
  176. "item_name": "priority",
  177. "item_optional": true,
  178. "item_type": "integer"
  179. }
  180. ]
  181. }
  182. }
  183. ],
  184. "commands": [
  185. {
  186. "command_name": "shutdown",
  187. "command_description": "Shut down BIND 10",
  188. "command_args": []
  189. },
  190. {
  191. "command_name": "ping",
  192. "command_description": "Ping the boss process",
  193. "command_args": []
  194. },
  195. {
  196. "command_name": "show_processes",
  197. "command_description": "List the running BIND 10 processes",
  198. "command_args": []
  199. }
  200. ],
  201. "statistics": [
  202. {
  203. "item_name": "boot_time",
  204. "item_type": "string",
  205. "item_optional": false,
  206. "item_default": "1970-01-01T00:00:00Z",
  207. "item_title": "Boot time",
  208. "item_description": "A date time when bind10 process starts initially",
  209. "item_format": "date-time"
  210. }
  211. ]
  212. }
  213. }
  214. """
  215. _BASETIME = (2011, 6, 22, 8, 14, 8, 2, 173, 0)
  216. def __init__(self):
  217. self._started = threading.Event()
  218. self.running = False
  219. self.spec_file = io.StringIO(self.spec_str)
  220. # create ModuleCCSession object
  221. self.mccs = isc.config.ModuleCCSession(
  222. self.spec_file,
  223. self.config_handler,
  224. self.command_handler)
  225. self.spec_file.close()
  226. self.cc_session = self.mccs._session
  227. self.got_command_name = ''
  228. self.pid_list = [[ 9999, "b10-auth", "Auth" ],
  229. [ 9998, "b10-auth-2", "Auth" ]]
  230. self.statistics_data = {
  231. 'boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', self._BASETIME)
  232. }
  233. def run(self):
  234. self.mccs.start()
  235. self.running = True
  236. self._started.set()
  237. try:
  238. while self.running:
  239. self.mccs.check_command(False)
  240. except Exception:
  241. pass
  242. def shutdown(self):
  243. self.running = False
  244. def config_handler(self, new_config):
  245. return isc.config.create_answer(0)
  246. def command_handler(self, command, *args, **kwargs):
  247. self._started.set()
  248. self.got_command_name = command
  249. sdata = self.statistics_data
  250. if command == 'getstats':
  251. return isc.config.create_answer(0, sdata)
  252. elif command == 'show_processes':
  253. # Return dummy pids
  254. return isc.config.create_answer(
  255. 0, self.pid_list)
  256. return isc.config.create_answer(1, "Unknown Command")
  257. class MockAuth:
  258. spec_str = """\
  259. {
  260. "module_spec": {
  261. "module_name": "Auth",
  262. "module_description": "Mock Authoritative service",
  263. "config_data": [],
  264. "commands": [],
  265. "statistics": [
  266. {
  267. "item_name": "queries.tcp",
  268. "item_type": "integer",
  269. "item_optional": false,
  270. "item_default": 0,
  271. "item_title": "Queries TCP",
  272. "item_description": "A number of total query counts which all auth servers receive over TCP since they started initially"
  273. },
  274. {
  275. "item_name": "queries.udp",
  276. "item_type": "integer",
  277. "item_optional": false,
  278. "item_default": 0,
  279. "item_title": "Queries UDP",
  280. "item_description": "A number of total query counts which all auth servers receive over UDP since they started initially"
  281. },
  282. {
  283. "item_name": "queries.perzone",
  284. "item_type": "list",
  285. "item_optional": false,
  286. "item_default": [
  287. {
  288. "zonename" : "test1.example",
  289. "queries.udp" : 1,
  290. "queries.tcp" : 2
  291. },
  292. {
  293. "zonename" : "test2.example",
  294. "queries.udp" : 3,
  295. "queries.tcp" : 4
  296. }
  297. ],
  298. "item_title": "Queries per zone",
  299. "item_description": "Queries per zone",
  300. "list_item_spec": {
  301. "item_name": "zones",
  302. "item_type": "map",
  303. "item_optional": false,
  304. "item_default": {},
  305. "map_item_spec": [
  306. {
  307. "item_name": "zonename",
  308. "item_type": "string",
  309. "item_optional": false,
  310. "item_default": "",
  311. "item_title": "Zonename",
  312. "item_description": "Zonename"
  313. },
  314. {
  315. "item_name": "queries.udp",
  316. "item_type": "integer",
  317. "item_optional": false,
  318. "item_default": 0,
  319. "item_title": "Queries UDP per zone",
  320. "item_description": "A number of UDP query counts per zone"
  321. },
  322. {
  323. "item_name": "queries.tcp",
  324. "item_type": "integer",
  325. "item_optional": false,
  326. "item_default": 0,
  327. "item_title": "Queries TCP per zone",
  328. "item_description": "A number of TCP query counts per zone"
  329. }
  330. ]
  331. }
  332. }
  333. ]
  334. }
  335. }
  336. """
  337. def __init__(self):
  338. self._started = threading.Event()
  339. self.running = False
  340. self.spec_file = io.StringIO(self.spec_str)
  341. # create ModuleCCSession object
  342. self.mccs = isc.config.ModuleCCSession(
  343. self.spec_file,
  344. self.config_handler,
  345. self.command_handler)
  346. self.spec_file.close()
  347. self.cc_session = self.mccs._session
  348. self.got_command_name = ''
  349. self.queries_tcp = 3
  350. self.queries_udp = 2
  351. self.queries_per_zone = [{
  352. 'zonename': 'test1.example',
  353. 'queries.tcp': 5,
  354. 'queries.udp': 4
  355. }]
  356. def run(self):
  357. self.mccs.start()
  358. self.running = True
  359. self._started.set()
  360. try:
  361. while self.running:
  362. self.mccs.check_command(False)
  363. except Exception:
  364. pass
  365. def shutdown(self):
  366. self.running = False
  367. def config_handler(self, new_config):
  368. return isc.config.create_answer(0)
  369. def command_handler(self, command, *args, **kwargs):
  370. self.got_command_name = command
  371. sdata = { 'queries.tcp': self.queries_tcp,
  372. 'queries.udp': self.queries_udp,
  373. 'queries.perzone' : self.queries_per_zone }
  374. if command == 'getstats':
  375. return isc.config.create_answer(0, sdata)
  376. return isc.config.create_answer(1, "Unknown Command")
  377. class MyStats(stats.Stats):
  378. def __init__(self):
  379. self._started = threading.Event()
  380. stats.Stats.__init__(self)
  381. def run(self):
  382. self._started.set()
  383. try:
  384. self.start()
  385. except Exception:
  386. pass
  387. def shutdown(self):
  388. self.command_shutdown()
  389. class MyStatsHttpd(stats_httpd.StatsHttpd):
  390. ORIG_SPECFILE_LOCATION = stats_httpd.SPECFILE_LOCATION
  391. def __init__(self, *server_address):
  392. self._started = threading.Event()
  393. if server_address:
  394. stats_httpd.SPECFILE_LOCATION = self.create_specfile(*server_address)
  395. try:
  396. stats_httpd.StatsHttpd.__init__(self)
  397. finally:
  398. if hasattr(stats_httpd.SPECFILE_LOCATION, "close"):
  399. stats_httpd.SPECFILE_LOCATION.close()
  400. stats_httpd.SPECFILE_LOCATION = self.ORIG_SPECFILE_LOCATION
  401. else:
  402. stats_httpd.StatsHttpd.__init__(self)
  403. def create_specfile(self, *server_address):
  404. spec_io = open(self.ORIG_SPECFILE_LOCATION)
  405. try:
  406. spec = json.load(spec_io)
  407. spec_io.close()
  408. config = spec['module_spec']['config_data']
  409. for i in range(len(config)):
  410. if config[i]['item_name'] == 'listen_on':
  411. config[i]['item_default'] = \
  412. [ dict(address=a[0], port=a[1]) for a in server_address ]
  413. break
  414. return io.StringIO(json.dumps(spec))
  415. finally:
  416. spec_io.close()
  417. def run(self):
  418. self._started.set()
  419. try:
  420. self.start()
  421. except Exception:
  422. pass
  423. def shutdown(self):
  424. self.command_handler('shutdown', None)
  425. class BaseModules:
  426. def __init__(self):
  427. # MockMsgq
  428. self.msgq = ThreadingServerManager(MockMsgq)
  429. self.msgq.run()
  430. # Check whether msgq is ready. A SessionTimeout is raised here if not.
  431. isc.cc.session.Session().close()
  432. # MockCfgmgr
  433. self.cfgmgr = ThreadingServerManager(MockCfgmgr)
  434. self.cfgmgr.run()
  435. # MockBoss
  436. self.boss = ThreadingServerManager(MockBoss)
  437. self.boss.run()
  438. # MockAuth
  439. self.auth = ThreadingServerManager(MockAuth)
  440. self.auth.run()
  441. self.auth2 = ThreadingServerManager(MockAuth)
  442. self.auth2.run()
  443. def shutdown(self):
  444. # MockAuth
  445. self.auth2.shutdown()
  446. self.auth.shutdown()
  447. # MockBoss
  448. self.boss.shutdown()
  449. # MockCfgmgr
  450. self.cfgmgr.shutdown()
  451. # MockMsgq
  452. self.msgq.shutdown()
  453. # remove the unused socket file
  454. socket_file = self.msgq.server.msgq.socket_file
  455. try:
  456. if os.path.exists(socket_file):
  457. os.remove(socket_file)
  458. except OSError:
  459. pass