test_utils.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  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 threading
  22. import json
  23. import signal
  24. import socket
  25. import isc.config.cfgmgr
  26. import stats
  27. import stats_httpd
  28. CONST_BASETIME = (2011, 6, 22, 8, 14, 8, 2, 173, 0)
  29. class SignalHandler():
  30. """A signal handler class for deadlock in unittest"""
  31. def __init__(self, fail_handler, timeout=20):
  32. """sets a schedule in SIGARM for invoking the handler via
  33. unittest.TestCase after timeout seconds (default is 20)"""
  34. self.fail_handler = fail_handler
  35. self.orig_handler = signal.signal(signal.SIGALRM, self.sig_handler)
  36. signal.alarm(timeout)
  37. def reset(self):
  38. """resets the schedule in SIGALRM"""
  39. signal.alarm(0)
  40. signal.signal(signal.SIGALRM, self.orig_handler)
  41. def sig_handler(self, signal, frame):
  42. """invokes unittest.TestCase.fail as a signal handler"""
  43. self.fail_handler("A deadlock might be detected")
  44. class ThreadingServerManager:
  45. def __init__(self, server, *args, **kwargs):
  46. self.server = server(*args, **kwargs)
  47. self.server_name = server.__name__
  48. self.server._thread = threading.Thread(
  49. name=self.server_name, target=self.server.run)
  50. self.server._thread.daemon = True
  51. def run(self):
  52. self.server._thread.start()
  53. self.server._started.wait()
  54. self.server._started.clear()
  55. def shutdown(self, blocking=False):
  56. """Shut down the server by calling its own shutdown() method.
  57. Then wait for its thread to finish. If blocking is True,
  58. the thread.join() blocks until the thread finishes. If not,
  59. it uses a zero timeout. The latter is necessary in a number
  60. of existing tests. We should redo this part (we should not
  61. even need threads in most, if not all, of these threads, see
  62. ticket #1668)"""
  63. self.server.shutdown()
  64. if blocking:
  65. self.server._thread.join()
  66. else:
  67. self.server._thread.join(0) # timeout is 0
  68. INIT_SPEC_STR = """\
  69. {
  70. "module_spec": {
  71. "module_name": "Init",
  72. "module_description": "Mock Master process",
  73. "config_data": [
  74. {
  75. "item_name": "components",
  76. "item_type": "named_set",
  77. "item_optional": false,
  78. "item_default": {
  79. "b10-stats": { "address": "Stats", "kind": "dispensable" },
  80. "b10-cmdctl": { "special": "cmdctl", "kind": "needed" }
  81. },
  82. "named_set_item_spec": {
  83. "item_name": "component",
  84. "item_type": "map",
  85. "item_optional": false,
  86. "item_default": { },
  87. "map_item_spec": [
  88. {
  89. "item_name": "special",
  90. "item_optional": true,
  91. "item_type": "string"
  92. },
  93. {
  94. "item_name": "process",
  95. "item_optional": true,
  96. "item_type": "string"
  97. },
  98. {
  99. "item_name": "kind",
  100. "item_optional": false,
  101. "item_type": "string",
  102. "item_default": "dispensable"
  103. },
  104. {
  105. "item_name": "address",
  106. "item_optional": true,
  107. "item_type": "string"
  108. },
  109. {
  110. "item_name": "params",
  111. "item_optional": true,
  112. "item_type": "list",
  113. "list_item_spec": {
  114. "item_name": "param",
  115. "item_optional": false,
  116. "item_type": "string",
  117. "item_default": ""
  118. }
  119. },
  120. {
  121. "item_name": "priority",
  122. "item_optional": true,
  123. "item_type": "integer"
  124. }
  125. ]
  126. }
  127. }
  128. ],
  129. "commands": [
  130. {
  131. "command_name": "shutdown",
  132. "command_description": "Shut down BIND 10",
  133. "command_args": []
  134. },
  135. {
  136. "command_name": "ping",
  137. "command_description": "Ping the b10-init process",
  138. "command_args": []
  139. },
  140. {
  141. "command_name": "show_processes",
  142. "command_description": "List the running BIND 10 processes",
  143. "command_args": []
  144. }
  145. ],
  146. "statistics": [
  147. {
  148. "item_name": "boot_time",
  149. "item_type": "string",
  150. "item_optional": false,
  151. "item_default": "1970-01-01T00:00:00Z",
  152. "item_title": "Boot time",
  153. "item_description": "A date time when bind10 process starts initially",
  154. "item_format": "date-time"
  155. }
  156. ]
  157. }
  158. }
  159. """
  160. # Note: this is derived of the spec for the DNS authoritative server, but
  161. # for the purpose of this test, it's completely irrelevant to DNS.
  162. # Some statisittics specs do not make sense for practical sense but used
  163. # just cover various types of statistics data (list, map/dict, etc).
  164. AUTH_SPEC_STR = """\
  165. {
  166. "module_spec": {
  167. "module_name": "Auth",
  168. "module_description": "Mock Authoritative service",
  169. "config_data": [],
  170. "commands": [],
  171. "statistics": [
  172. {
  173. "item_name": "queries.tcp",
  174. "item_type": "integer",
  175. "item_optional": false,
  176. "item_default": 0,
  177. "item_title": "Queries TCP",
  178. "item_description": "A number of total query counts which all auth servers receive over TCP since they started initially"
  179. },
  180. {
  181. "item_name": "queries.udp",
  182. "item_type": "integer",
  183. "item_optional": false,
  184. "item_default": 0,
  185. "item_title": "Queries UDP",
  186. "item_description": "A number of total query counts which all auth servers receive over UDP since they started initially"
  187. },
  188. {
  189. "item_name": "queries.perzone",
  190. "item_type": "list",
  191. "item_optional": false,
  192. "item_default": [
  193. {
  194. "zonename" : "test1.example",
  195. "queries.udp" : 1,
  196. "queries.tcp" : 2
  197. },
  198. {
  199. "zonename" : "test2.example",
  200. "queries.udp" : 3,
  201. "queries.tcp" : 4
  202. }
  203. ],
  204. "item_title": "Queries per zone",
  205. "item_description": "Queries per zone",
  206. "list_item_spec": {
  207. "item_name": "zones",
  208. "item_type": "map",
  209. "item_optional": false,
  210. "item_default": {},
  211. "map_item_spec": [
  212. {
  213. "item_name": "zonename",
  214. "item_type": "string",
  215. "item_optional": false,
  216. "item_default": "",
  217. "item_title": "Zonename",
  218. "item_description": "Zonename"
  219. },
  220. {
  221. "item_name": "queries.udp",
  222. "item_type": "integer",
  223. "item_optional": false,
  224. "item_default": 0,
  225. "item_title": "Queries UDP per zone",
  226. "item_description": "A number of UDP query counts per zone"
  227. },
  228. {
  229. "item_name": "queries.tcp",
  230. "item_type": "integer",
  231. "item_optional": false,
  232. "item_default": 0,
  233. "item_title": "Queries TCP per zone",
  234. "item_description": "A number of TCP query counts per zone"
  235. }
  236. ]
  237. }
  238. },
  239. {
  240. "item_name": "nds_queries.perzone",
  241. "item_type": "named_set",
  242. "item_optional": false,
  243. "item_default": {
  244. "test10.example" : {
  245. "queries.udp" : 1,
  246. "queries.tcp" : 2
  247. },
  248. "test20.example" : {
  249. "queries.udp" : 3,
  250. "queries.tcp" : 4
  251. }
  252. },
  253. "item_title": "Queries per zone",
  254. "item_description": "Queries per zone",
  255. "named_set_item_spec": {
  256. "item_name": "zonename",
  257. "item_type": "map",
  258. "item_optional": false,
  259. "item_default": {},
  260. "item_title": "Zonename",
  261. "item_description": "Zonename",
  262. "map_item_spec": [
  263. {
  264. "item_name": "queries.udp",
  265. "item_type": "integer",
  266. "item_optional": false,
  267. "item_default": 0,
  268. "item_title": "Queries UDP per zone",
  269. "item_description": "A number of UDP query counts per zone"
  270. },
  271. {
  272. "item_name": "queries.tcp",
  273. "item_type": "integer",
  274. "item_optional": false,
  275. "item_default": 0,
  276. "item_title": "Queries TCP per zone",
  277. "item_description": "A number of TCP query counts per zone"
  278. }
  279. ]
  280. }
  281. }
  282. ]
  283. }
  284. }
  285. """
  286. class MyModuleCCSession(isc.config.ConfigData):
  287. """Mocked ModuleCCSession class.
  288. This class incorporates the module spec directly from the file,
  289. and works as if the ModuleCCSession class as much as possible
  290. without involving network I/O.
  291. """
  292. def __init__(self, spec_file, config_handler, command_handler):
  293. module_spec = isc.config.module_spec_from_file(spec_file)
  294. isc.config.ConfigData.__init__(self, module_spec)
  295. self._session = self
  296. self.stopped = False
  297. self.closed = False
  298. self.lname = 'mock_mod_ccs'
  299. self._msg = None
  300. self._env = None
  301. def start(self):
  302. pass
  303. def send_stopping(self):
  304. self.stopped = True # just record it's called to inspect it later
  305. def close(self):
  306. self.closed = True
  307. def check_command_without_recvmsg(self, msg, env):
  308. self._msg = msg
  309. self._env = env
  310. class MyStats(stats.Stats):
  311. """A faked Stats class for unit tests.
  312. This class inherits most of the real Stats class, but replaces the
  313. ModuleCCSession with a fake one so we can avoid network I/O in tests,
  314. and can also inspect or tweak messages via the session more easily.
  315. This class also maintains some faked module information and statistics
  316. data that can be retrieved from the implementation of the Stats class.
  317. """
  318. def __init__(self):
  319. # First, setup some internal attributes. All of them are essentially
  320. # private (so prefixed with double '_'), but some are defined as if
  321. # "protected" (with a single '_') for the convenient of tests that
  322. # may want to inspect or tweak them.
  323. # initial seq num for faked group_sendmsg, arbitrary choice.
  324. self._seq = 4200
  325. # if set, use them as faked response to group_recvmsg (see below).
  326. # it's a list of tuples, each of which is of (answer, envelope).
  327. self._answers = []
  328. # the default answer from faked recvmsg if _answers is empty
  329. self.__default_answer = isc.config.ccsession.create_answer(
  330. 0, {'Init':
  331. json.loads(INIT_SPEC_STR)['module_spec']['statistics'],
  332. 'Auth':
  333. json.loads(AUTH_SPEC_STR)['module_spec']['statistics']
  334. })
  335. # setup faked auth statistics
  336. self.__init_auth_stat()
  337. # statistics data for faked Init module
  338. self._init_sdata = {
  339. 'boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME)
  340. }
  341. # Incorporate other setups of the real Stats module. We use the faked
  342. # ModuleCCSession to avoid blocking network operation. Note also that
  343. # we replace _init_statistics_data() (see below), so we don't
  344. # initialize statistics data yet.
  345. stats.Stats.__init__(self, MyModuleCCSession)
  346. # replace some (faked) ModuleCCSession methods so we can inspect/fake
  347. # the data exchanged via the CC session, then call
  348. # _init_statistics_data. This will get the Stats module info from
  349. # the file directly and some amount information about the Init and
  350. # Auth modules (hardcoded below).
  351. self.cc_session.group_sendmsg = self.__group_sendmsg
  352. self.cc_session.group_recvmsg = self.__group_recvmsg
  353. self.cc_session.rpc_call = self.__rpc_call
  354. stats.Stats._init_statistics_data(self)
  355. def __init_auth_stat(self):
  356. self._queries_tcp = 3
  357. self._queries_udp = 2
  358. self._queries_per_zone = [{
  359. 'zonename': 'test1.example', 'queries.tcp': 5, 'queries.udp': 4
  360. }]
  361. self._nds_queries_per_zone = \
  362. { 'test10.example': { 'queries.tcp': 5, 'queries.udp': 4 } }
  363. self._auth_sdata = \
  364. { 'queries.tcp': self._queries_tcp,
  365. 'queries.udp': self._queries_udp,
  366. 'queries.perzone' : self._queries_per_zone,
  367. 'nds_queries.perzone' : {
  368. 'test10.example': {
  369. 'queries.tcp': isc.cc.data.find(
  370. self._nds_queries_per_zone,
  371. 'test10.example/queries.tcp')
  372. }
  373. },
  374. 'nds_queries.perzone/test10.example/queries.udp' :
  375. isc.cc.data.find(self._nds_queries_per_zone,
  376. 'test10.example/queries.udp')
  377. }
  378. def _init_statistics_data(self):
  379. # Inherited from real Stats class, just for deferring the
  380. # initialization until we are ready.
  381. pass
  382. def __group_sendmsg(self, command, destination, want_answer=False):
  383. """Faked ModuleCCSession.group_sendmsg for tests.
  384. Skipping actual network communication, and just returning an internally
  385. generated sequence number.
  386. """
  387. self._seq += 1
  388. return self._seq
  389. def __group_recvmsg(self, nonblocking = True, seq = None):
  390. """Faked ModuleCCSession.group_recvmsg for tests.
  391. Skipping actual network communication, and returning an internally
  392. prepared answer. sequence number. If faked anser is given in
  393. _answers, use it; otherwise use the default. we don't actually check
  394. the sequence.
  395. """
  396. if len(self._answers) == 0:
  397. return self.__default_answer, {'from': 'no-matter'}
  398. return self._answers.pop(0)
  399. def __rpc_call(self, command, group):
  400. """Faked ModuleCCSession.rpc_call for tests.
  401. At the moment we don't have to cover failure cases, so this is a
  402. simple wrapper for the faked group_recvmsg().
  403. """
  404. answer, _ = self.__group_recvmsg(None, None)
  405. return isc.config.ccsession.parse_answer(answer)[1]
  406. class MyStatsHttpd(stats_httpd.StatsHttpd):
  407. """A faked StatsHttpd class for unit tests.
  408. This class inherits most of the real StatsHttpd class, but replaces the
  409. ModuleCCSession with a fake one so we can avoid network I/O in tests,
  410. and can also inspect or tweak messages via the session more easily.
  411. """
  412. ORIG_SPECFILE_LOCATION = stats_httpd.SPECFILE_LOCATION
  413. def __init__(self, *server_address):
  414. self._started = threading.Event()
  415. self.__dummy_sock = None # see below
  416. # Prepare commonly used statistics schema and data requested in
  417. # stats-httpd tests. For the purpose of these tests, the content of
  418. # statistics data is not so important (they don't test whther the
  419. # counter values are correct, etc), so hardcoding the common case
  420. # should suffice. Note also that some of the statistics values and
  421. # specs don't make sense in practice (see also comments on
  422. # AUTH_SPEC_STR).
  423. with open(stats.SPECFILE_LOCATION) as f:
  424. stat_spec_str = f.read()
  425. self.__default_spec_answer = {
  426. 'Init': json.loads(INIT_SPEC_STR)['module_spec']['statistics'],
  427. 'Auth': json.loads(AUTH_SPEC_STR)['module_spec']['statistics'],
  428. 'Stats': json.loads(stat_spec_str)['module_spec']['statistics']
  429. }
  430. self.__default_data_answer = {
  431. 'Init': {'boot_time':
  432. time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME)},
  433. 'Stats': {'last_update_time':
  434. time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME),
  435. 'report_time':
  436. time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME),
  437. 'lname': 'test-lname',
  438. 'boot_time':
  439. time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME),
  440. 'timestamp': time.mktime(CONST_BASETIME)},
  441. 'Auth': {'queries.udp': 4, 'queries.tcp': 6,
  442. 'queries.perzone': [
  443. {'queries.udp': 8, 'queries.tcp': 10,
  444. 'zonename': 'test1.example'},
  445. {'queries.udp': 6, 'queries.tcp': 8,
  446. 'zonename': 'test2.example'}],
  447. 'nds_queries.perzone': {
  448. 'test10.example': {'queries.udp': 8, 'queries.tcp': 10},
  449. 'test20.example': {'queries.udp': 6, 'queries.tcp': 8}}}}
  450. # if set, use them as faked response to rpc_call (see below).
  451. # it's a list of answer data of rpc_call.
  452. self._rpc_answers = []
  453. if server_address:
  454. stats_httpd.SPECFILE_LOCATION = \
  455. self.__create_specfile(*server_address)
  456. try:
  457. stats_httpd.StatsHttpd.__init__(self)
  458. finally:
  459. if hasattr(stats_httpd.SPECFILE_LOCATION, "close"):
  460. stats_httpd.SPECFILE_LOCATION.close()
  461. stats_httpd.SPECFILE_LOCATION = self.ORIG_SPECFILE_LOCATION
  462. else:
  463. stats_httpd.StatsHttpd.__init__(self)
  464. # replace some (faked) ModuleCCSession methods so we can inspect/fake.
  465. # in order to satisfy select.select() we need some real socket. We
  466. # use an unusable AF_UNIX socket; we won't actually use it for
  467. # communication.
  468. self.cc_session.rpc_call = self.__rpc_call
  469. self.__dummy_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  470. self.mccs.get_socket = lambda: self.__dummy_sock
  471. def open_mccs(self):
  472. self.mccs = MyModuleCCSession(stats_httpd.SPECFILE_LOCATION,
  473. self.config_handler,
  474. self.command_handler)
  475. self.cc_session = self.mccs._session
  476. self.mccs.start = self.load_config # force reload
  477. # check_command could be called from the main select() loop due to
  478. # Linux's bug of spurious wakeup. We don't need the actual behavior
  479. # of check_command in our tests, so we can basically replace it with a
  480. # no-op mock function.
  481. def mock_check_command(nonblock):
  482. pass
  483. self.mccs.check_command = mock_check_command
  484. def close_mccs(self):
  485. super().close_mccs()
  486. if self.__dummy_sock is not None:
  487. self.__dummy_sock.close()
  488. self.__dummy_sock = None
  489. def __rpc_call(self, command, group, params={}):
  490. """Faked ModuleCCSession.rpc_call for tests.
  491. The stats httpd module only issues two commands: 'showschema' and
  492. 'show'. In most cases we can simply use the prepared default
  493. answer. If customization is needed, the test case can add a
  494. faked answer by appending it to _rpc_answers. If the added object
  495. is of Exception type this method raises it instead of return it,
  496. emulating the situation where rpc_call() results in an exception.
  497. """
  498. if len(self._rpc_answers) == 0:
  499. if command == 'showschema':
  500. return self.__default_spec_answer
  501. elif command == 'show':
  502. return self.__default_data_answer
  503. assert False, "unexpected command for faked rpc_call: " + command
  504. answer = self._rpc_answers.pop(0)
  505. if issubclass(type(answer), Exception):
  506. raise answer
  507. return answer
  508. def __create_specfile(self, *server_address):
  509. spec_io = open(self.ORIG_SPECFILE_LOCATION)
  510. try:
  511. spec = json.load(spec_io)
  512. spec_io.close()
  513. config = spec['module_spec']['config_data']
  514. for i in range(len(config)):
  515. if config[i]['item_name'] == 'listen_on':
  516. config[i]['item_default'] = \
  517. [ dict(address=a[0], port=a[1])
  518. for a in server_address ]
  519. break
  520. return io.StringIO(json.dumps(spec))
  521. finally:
  522. spec_io.close()
  523. def run(self):
  524. self._started.set()
  525. self.start()
  526. def shutdown(self):
  527. self.command_handler('shutdown', None)