b10-stats-httpd_test.py 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971
  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. In each of these tests we start several virtual components. They are
  17. not the real components, no external processes are started. They are
  18. just simple mock objects running each in its own thread and pretending
  19. to be bind10 modules. This helps testing the stats http server in a
  20. close to real environment.
  21. """
  22. import unittest
  23. import os
  24. import imp
  25. import socket
  26. import errno
  27. import select
  28. import string
  29. import time
  30. import threading
  31. import http.client
  32. import xml.etree.ElementTree
  33. import random
  34. import urllib.parse
  35. import isc
  36. import stats_httpd
  37. import stats
  38. from test_utils import BaseModules, ThreadingServerManager, MyStats,\
  39. MyStatsHttpd, SignalHandler,\
  40. send_command, send_shutdown, CONST_BASETIME
  41. from isc.testutils.ccsession_mock import MockModuleCCSession
  42. # set XML Namespaces for testing
  43. XMLNS_XSL = "http://www.w3.org/1999/XSL/Transform"
  44. XMLNS_XHTML = "http://www.w3.org/1999/xhtml"
  45. XMLNS_XSD = "http://www.w3.org/2001/XMLSchema"
  46. XMLNS_XSI = stats_httpd.XMLNS_XSI
  47. DUMMY_DATA = {
  48. 'Boss' : {
  49. "boot_time": time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME)
  50. },
  51. 'Auth' : {
  52. "queries.tcp": 6,
  53. "queries.udp": 4,
  54. "queries.perzone": [{
  55. "zonename": "test1.example",
  56. "queries.tcp": 10,
  57. "queries.udp": 8
  58. }, {
  59. "zonename": "test2.example",
  60. "queries.tcp": 8,
  61. "queries.udp": 6
  62. }],
  63. "nds_queries.perzone": {
  64. "test10.example": {
  65. "queries.tcp": 10,
  66. "queries.udp": 8
  67. },
  68. "test20.example": {
  69. "queries.tcp": 8,
  70. "queries.udp": 6
  71. }
  72. }
  73. },
  74. 'Stats' : {
  75. "report_time": time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME),
  76. "boot_time": time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME),
  77. "last_update_time": time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME),
  78. "lname": "4d70d40a_c@host",
  79. "timestamp": time.mktime(CONST_BASETIME)
  80. }
  81. }
  82. def get_availaddr(address='127.0.0.1', port=8001):
  83. """returns a tuple of address and port which is available to
  84. listen on the platform. The first argument is a address for
  85. search. The second argument is a port for search. If a set of
  86. address and port is failed on the search for the availability, the
  87. port number is increased and it goes on the next trial until the
  88. available set of address and port is looked up. If the port number
  89. reaches over 65535, it may stop the search and raise a
  90. OverflowError exception."""
  91. while True:
  92. for addr in socket.getaddrinfo(
  93. address, port, 0,
  94. socket.SOCK_STREAM, socket.IPPROTO_TCP):
  95. sock = socket.socket(addr[0], socket.SOCK_STREAM)
  96. try:
  97. sock.bind((address, port))
  98. return (address, port)
  99. except socket.error:
  100. continue
  101. finally:
  102. if sock: sock.close()
  103. # This address and port number are already in use.
  104. # next port number is added
  105. port = port + 1
  106. def is_ipv6_enabled(address='::1', port=8001):
  107. """checks IPv6 enabled on the platform. address for check is '::1'
  108. and port for check is random number between 8001 and
  109. 65535. Retrying is 3 times even if it fails. The built-in socket
  110. module provides a 'has_ipv6' parameter, but it's not used here
  111. because there may be a situation where the value is True on an
  112. environment where the IPv6 config is disabled."""
  113. for p in random.sample(range(port, 65535), 3):
  114. try:
  115. sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
  116. sock.bind((address, p))
  117. return True
  118. except socket.error:
  119. continue
  120. finally:
  121. if sock: sock.close()
  122. return False
  123. class TestItemNameList(unittest.TestCase):
  124. def test_item_name_list(self):
  125. self.assertEqual(['a'],
  126. stats_httpd.item_name_list({'a':1}, 'a'))
  127. self.assertEqual(['a','a/b'],
  128. stats_httpd.item_name_list({'a':{'b':1}}, 'a'))
  129. self.assertEqual(['a/b'],
  130. stats_httpd.item_name_list({'a':{'b':1}}, 'a/b'))
  131. self.assertEqual(['a','a/b','a/b/c'],
  132. stats_httpd.item_name_list({'a':{'b':{'c':1}}}, 'a'))
  133. self.assertEqual(['a/b','a/b/c'],
  134. stats_httpd.item_name_list({'a':{'b':{'c':1}}}, 'a/b'))
  135. self.assertEqual(['a/b/c'],
  136. stats_httpd.item_name_list({'a':{'b':{'c':1}}}, 'a/b/c'))
  137. self.assertEqual(['a[2]'],
  138. stats_httpd.item_name_list({'a':[1,2,3]}, 'a[2]'))
  139. self.assertEqual(['a', 'a[0]', 'a[1]', 'a[2]'],
  140. stats_httpd.item_name_list({'a':[1,2,3]}, 'a'))
  141. self.assertEqual(['a', 'a[0]', 'a[1]', 'a[2]'],
  142. stats_httpd.item_name_list({'a':[1,2,3]}, ''))
  143. self.assertEqual(['a', 'a/b', 'a/b[0]', 'a/b[1]', 'a/b[2]'],
  144. stats_httpd.item_name_list({'a':{'b':[1,2,3]}}, 'a'))
  145. self.assertEqual(['a', 'a/b', 'a/b[0]', 'a/b[1]', 'a/b[2]'],
  146. stats_httpd.item_name_list({'a':{'b':[1,2,3]}}, ''))
  147. self.assertEqual(['a/b', 'a/b[0]', 'a/b[1]', 'a/b[2]'],
  148. stats_httpd.item_name_list({'a':{'b':[1,2,3]}}, 'a/b'))
  149. self.assertEqual(['a', 'a/b', 'a/b[0]', 'a/b[1]', 'a/b[2]', 'a/c'],
  150. stats_httpd.item_name_list(
  151. {'a':{'b':[1,2,3], 'c':1}}, 'a'))
  152. self.assertEqual(['a/b', 'a/b[0]', 'a/b[1]', 'a/b[2]'],
  153. stats_httpd.item_name_list(
  154. {'a':{'b':[1,2,3], 'c':1}}, 'a/b'))
  155. self.assertEqual(['a/c'],
  156. stats_httpd.item_name_list(
  157. {'a':{'b':[1,2,3], 'c':1}}, 'a/c'))
  158. self.assertRaises(isc.cc.data.DataNotFoundError,
  159. stats_httpd.item_name_list, {'x':1}, 'a')
  160. self.assertEqual([],
  161. stats_httpd.item_name_list('a', ''))
  162. self.assertEqual([],
  163. stats_httpd.item_name_list('', ''))
  164. self.assertRaises(isc.cc.data.DataTypeError,
  165. stats_httpd.item_name_list, 'a', 'a')
  166. self.assertRaises(isc.cc.data.DataTypeError,
  167. stats_httpd.item_name_list, None, None)
  168. self.assertRaises(isc.cc.data.DataTypeError,
  169. stats_httpd.item_name_list, [1,2,3], 'a')
  170. self.assertRaises(isc.cc.data.DataTypeError,
  171. stats_httpd.item_name_list, [1,2,3], '')
  172. class TestHttpHandler(unittest.TestCase):
  173. """Tests for HttpHandler class"""
  174. def setUp(self):
  175. # set the signal handler for deadlock
  176. self.sig_handler = SignalHandler(self.fail)
  177. self.base = BaseModules()
  178. self.stats_server = ThreadingServerManager(MyStats)
  179. self.stats = self.stats_server.server
  180. DUMMY_DATA['Stats']['lname'] = self.stats.cc_session.lname
  181. self.stats_server.run()
  182. (self.address, self.port) = get_availaddr()
  183. self.stats_httpd_server = ThreadingServerManager(MyStatsHttpd, (self.address, self.port))
  184. self.stats_httpd = self.stats_httpd_server.server
  185. self.stats_httpd_server.run()
  186. self.client = http.client.HTTPConnection(self.address, self.port)
  187. self.client._http_vsn_str = 'HTTP/1.0\n'
  188. self.client.connect()
  189. def tearDown(self):
  190. self.client.close()
  191. self.stats_httpd_server.shutdown()
  192. self.stats_server.shutdown()
  193. self.base.shutdown()
  194. # reset the signal handler
  195. self.sig_handler.reset()
  196. def test_do_GET(self):
  197. self.assertTrue(type(self.stats_httpd.httpd) is list)
  198. self.assertEqual(len(self.stats_httpd.httpd), 1)
  199. self.assertEqual((self.address, self.port), self.stats_httpd.http_addrs[0])
  200. def check_XML_URL_PATH(path=''):
  201. url_path = '%s/%s' % (stats_httpd.XML_URL_PATH, path)
  202. url_path = urllib.parse.quote(url_path)
  203. self.client.putrequest('GET', url_path)
  204. self.client.endheaders()
  205. response = self.client.getresponse()
  206. self.assertEqual(response.getheader("Content-type"), "text/xml")
  207. self.assertGreater(int(response.getheader("Content-Length")), 0)
  208. self.assertEqual(response.status, 200)
  209. xml_doctype = response.readline().decode()
  210. xsl_doctype = response.readline().decode()
  211. self.assertGreater(len(xml_doctype), 0)
  212. self.assertGreater(len(xsl_doctype), 0)
  213. root = xml.etree.ElementTree.parse(response).getroot()
  214. self.assertGreater(root.tag.find('statistics'), 0)
  215. schema_loc = '{%s}schemaLocation' % XMLNS_XSI
  216. # check the path of XSD
  217. self.assertEqual(root.attrib[schema_loc],
  218. stats_httpd.XSD_NAMESPACE + ' '
  219. + stats_httpd.XSD_URL_PATH)
  220. # check the path of XSL
  221. self.assertTrue(xsl_doctype.startswith(
  222. '<?xml-stylesheet type="text/xsl" href="' +
  223. stats_httpd.XSL_URL_PATH
  224. + '"?>'))
  225. for elem in root.find('item'):
  226. attr = elem.attrib
  227. value = isc.cc.data.find(DUMMY_DATA, attr['identifier'])
  228. if type(value) is list or type(value) is dict:
  229. self.assertFalse('value' in attr)
  230. elif type(value) is int or type(value) is float:
  231. self.assertEqual(attr['value'], str(value))
  232. else:
  233. self.assertEqual(attr['value'], value)
  234. # URL is '/bind10/statistics/xml'
  235. check_XML_URL_PATH()
  236. for path in stats_httpd.item_name_list(DUMMY_DATA, ''):
  237. check_XML_URL_PATH(path)
  238. def check_XSD_URL_PATH():
  239. url_path = stats_httpd.XSD_URL_PATH
  240. url_path = urllib.parse.quote(url_path)
  241. self.client.putrequest('GET', url_path)
  242. self.client.endheaders()
  243. response = self.client.getresponse()
  244. self.assertEqual(response.getheader("Content-type"), "text/xml")
  245. self.assertGreater(int(response.getheader("Content-Length")), 0)
  246. self.assertEqual(response.status, 200)
  247. root = xml.etree.ElementTree.parse(response).getroot()
  248. url_xmlschema = '{%s}' % XMLNS_XSD
  249. self.assertGreater(root.tag.find('schema'), 0)
  250. self.assertTrue(hasattr(root, 'attrib'))
  251. self.assertTrue('targetNamespace' in root.attrib)
  252. self.assertEqual(root.attrib['targetNamespace'],
  253. stats_httpd.XSD_NAMESPACE)
  254. # URL is '/bind10/statistics/xsd'
  255. check_XSD_URL_PATH()
  256. def check_XSL_URL_PATH():
  257. url_path = stats_httpd.XSL_URL_PATH
  258. url_path = urllib.parse.quote(url_path)
  259. self.client.putrequest('GET', url_path)
  260. self.client.endheaders()
  261. response = self.client.getresponse()
  262. self.assertEqual(response.getheader("Content-type"), "text/xml")
  263. self.assertGreater(int(response.getheader("Content-Length")), 0)
  264. self.assertEqual(response.status, 200)
  265. root = xml.etree.ElementTree.parse(response).getroot()
  266. url_trans = '{%s}' % XMLNS_XSL
  267. url_xhtml = '{%s}' % XMLNS_XHTML
  268. self.assertEqual(root.tag, url_trans + 'stylesheet')
  269. # URL is '/bind10/statistics/xsl'
  270. check_XSL_URL_PATH()
  271. # 302 redirect
  272. self.client._http_vsn_str = 'HTTP/1.1'
  273. self.client.putrequest('GET', '/')
  274. self.client.putheader('Host', self.address)
  275. self.client.endheaders()
  276. response = self.client.getresponse()
  277. self.assertEqual(response.status, 302)
  278. self.assertEqual(response.getheader('Location'),
  279. "http://%s:%d%s/" % (self.address, self.port, stats_httpd.XML_URL_PATH))
  280. # 404 NotFound (random path)
  281. self.client._http_vsn_str = 'HTTP/1.0'
  282. self.client.putrequest('GET', '/path/to/foo/bar')
  283. self.client.endheaders()
  284. response = self.client.getresponse()
  285. self.assertEqual(response.status, 404)
  286. self.client._http_vsn_str = 'HTTP/1.0'
  287. self.client.putrequest('GET', '/bind10/foo')
  288. self.client.endheaders()
  289. response = self.client.getresponse()
  290. self.assertEqual(response.status, 404)
  291. self.client._http_vsn_str = 'HTTP/1.0'
  292. self.client.putrequest('GET', '/bind10/statistics/foo')
  293. self.client.endheaders()
  294. response = self.client.getresponse()
  295. self.assertEqual(response.status, 404)
  296. self.client._http_vsn_str = 'HTTP/1.0'
  297. self.client.putrequest('GET', stats_httpd.XML_URL_PATH + 'Auth') # with no slash
  298. self.client.endheaders()
  299. response = self.client.getresponse()
  300. self.assertEqual(response.status, 404)
  301. # 200 ok
  302. self.client._http_vsn_str = 'HTTP/1.0'
  303. self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/')
  304. self.client.endheaders()
  305. response = self.client.getresponse()
  306. self.assertEqual(response.status, 200)
  307. self.client._http_vsn_str = 'HTTP/1.0'
  308. self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/#foo')
  309. self.client.endheaders()
  310. response = self.client.getresponse()
  311. self.assertEqual(response.status, 200)
  312. self.client._http_vsn_str = 'HTTP/1.0'
  313. self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/?foo=bar')
  314. self.client.endheaders()
  315. response = self.client.getresponse()
  316. self.assertEqual(response.status, 200)
  317. # 404 NotFound (too long path)
  318. self.client._http_vsn_str = 'HTTP/1.0'
  319. self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/Boss/boot_time/a')
  320. self.client.endheaders()
  321. response = self.client.getresponse()
  322. self.assertEqual(response.status, 404)
  323. # 404 NotFound (nonexistent module name)
  324. self.client._http_vsn_str = 'HTTP/1.0'
  325. self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/Foo')
  326. self.client.endheaders()
  327. response = self.client.getresponse()
  328. self.assertEqual(response.status, 404)
  329. self.client._http_vsn_str = 'HTTP/1.0'
  330. self.client.putrequest('GET', stats_httpd.XSD_URL_PATH + '/Foo')
  331. self.client.endheaders()
  332. response = self.client.getresponse()
  333. self.assertEqual(response.status, 404)
  334. self.client._http_vsn_str = 'HTTP/1.0'
  335. self.client.putrequest('GET', stats_httpd.XSL_URL_PATH + '/Foo')
  336. self.client.endheaders()
  337. response = self.client.getresponse()
  338. self.assertEqual(response.status, 404)
  339. # 404 NotFound (nonexistent item name)
  340. self.client._http_vsn_str = 'HTTP/1.0'
  341. self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/Foo/bar')
  342. self.client.endheaders()
  343. response = self.client.getresponse()
  344. self.assertEqual(response.status, 404)
  345. self.client._http_vsn_str = 'HTTP/1.0'
  346. self.client.putrequest('GET', stats_httpd.XSD_URL_PATH + '/Foo/bar')
  347. self.client.endheaders()
  348. response = self.client.getresponse()
  349. self.assertEqual(response.status, 404)
  350. self.client._http_vsn_str = 'HTTP/1.0'
  351. self.client.putrequest('GET', stats_httpd.XSL_URL_PATH + '/Foo/bar')
  352. self.client.endheaders()
  353. response = self.client.getresponse()
  354. self.assertEqual(response.status, 404)
  355. # 404 NotFound (existent module but nonexistent item name)
  356. self.client._http_vsn_str = 'HTTP/1.0'
  357. self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/Auth/bar')
  358. self.client.endheaders()
  359. response = self.client.getresponse()
  360. self.assertEqual(response.status, 404)
  361. self.client._http_vsn_str = 'HTTP/1.0'
  362. self.client.putrequest('GET', stats_httpd.XSD_URL_PATH + '/Auth/bar')
  363. self.client.endheaders()
  364. response = self.client.getresponse()
  365. self.assertEqual(response.status, 404)
  366. self.client._http_vsn_str = 'HTTP/1.0'
  367. self.client.putrequest('GET', stats_httpd.XSL_URL_PATH + '/Auth/bar')
  368. self.client.endheaders()
  369. response = self.client.getresponse()
  370. self.assertEqual(response.status, 404)
  371. def test_do_GET_failed1(self):
  372. # checks status
  373. self.assertEqual(send_command("status", "Stats"),
  374. (0, "Stats is up. (PID " + str(os.getpid()) + ")"))
  375. # failure case(Stats is down)
  376. self.assertTrue(self.stats.running)
  377. self.assertEqual(send_shutdown("Stats"), (0, None)) # Stats is down
  378. self.assertFalse(self.stats.running)
  379. self.stats_httpd.cc_session.set_timeout(milliseconds=100)
  380. # request XML
  381. self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/')
  382. self.client.endheaders()
  383. response = self.client.getresponse()
  384. self.assertEqual(response.status, 500)
  385. # request XSD
  386. self.client.putrequest('GET', stats_httpd.XSD_URL_PATH)
  387. self.client.endheaders()
  388. response = self.client.getresponse()
  389. self.assertEqual(response.status, 200)
  390. # request XSL
  391. self.client.putrequest('GET', stats_httpd.XSL_URL_PATH)
  392. self.client.endheaders()
  393. response = self.client.getresponse()
  394. self.assertEqual(response.status, 200)
  395. def test_do_GET_failed2(self):
  396. # failure case(Stats replies an error)
  397. self.stats.mccs.set_command_handler(
  398. lambda cmd, args: \
  399. isc.config.ccsession.create_answer(1, "specified arguments are incorrect: I have an error.")
  400. )
  401. # request XML
  402. self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/')
  403. self.client.endheaders()
  404. response = self.client.getresponse()
  405. self.assertEqual(response.status, 404)
  406. # request XSD
  407. self.client.putrequest('GET', stats_httpd.XSD_URL_PATH)
  408. self.client.endheaders()
  409. response = self.client.getresponse()
  410. self.assertEqual(response.status, 200)
  411. # request XSL
  412. self.client.putrequest('GET', stats_httpd.XSL_URL_PATH)
  413. self.client.endheaders()
  414. response = self.client.getresponse()
  415. self.assertEqual(response.status, 200)
  416. def test_do_HEAD(self):
  417. self.client.putrequest('HEAD', stats_httpd.XML_URL_PATH + '/')
  418. self.client.endheaders()
  419. response = self.client.getresponse()
  420. self.assertEqual(response.status, 200)
  421. self.client.putrequest('HEAD', '/path/to/foo/bar')
  422. self.client.endheaders()
  423. response = self.client.getresponse()
  424. self.assertEqual(response.status, 404)
  425. class TestHttpServerError(unittest.TestCase):
  426. """Tests for HttpServerError exception"""
  427. def test_raises(self):
  428. try:
  429. raise stats_httpd.HttpServerError('Nothing')
  430. except stats_httpd.HttpServerError as err:
  431. self.assertEqual(str(err), 'Nothing')
  432. class TestHttpServer(unittest.TestCase):
  433. """Tests for HttpServer class"""
  434. def setUp(self):
  435. # set the signal handler for deadlock
  436. self.sig_handler = SignalHandler(self.fail)
  437. self.base = BaseModules()
  438. def tearDown(self):
  439. if hasattr(self, "stats_httpd"):
  440. self.stats_httpd.stop()
  441. self.base.shutdown()
  442. # reset the signal handler
  443. self.sig_handler.reset()
  444. def test_httpserver(self):
  445. self.stats_httpd = MyStatsHttpd(get_availaddr())
  446. self.assertEqual(type(self.stats_httpd.httpd), list)
  447. self.assertEqual(len(self.stats_httpd.httpd), 1)
  448. for httpd in self.stats_httpd.httpd:
  449. self.assertTrue(isinstance(httpd, stats_httpd.HttpServer))
  450. class TestStatsHttpdError(unittest.TestCase):
  451. """Tests for StatsHttpdError exception"""
  452. def test_raises1(self):
  453. try:
  454. raise stats_httpd.StatsHttpdError('Nothing')
  455. except stats_httpd.StatsHttpdError as err:
  456. self.assertEqual(str(err), 'Nothing')
  457. def test_raises2(self):
  458. try:
  459. raise stats_httpd.StatsHttpdDataError('Nothing')
  460. except stats_httpd.StatsHttpdDataError as err:
  461. self.assertEqual(str(err), 'Nothing')
  462. class TestStatsHttpd(unittest.TestCase):
  463. """Tests for StatsHttpd class"""
  464. def setUp(self):
  465. # set the signal handler for deadlock
  466. self.sig_handler = SignalHandler(self.fail)
  467. self.base = BaseModules()
  468. self.stats_server = ThreadingServerManager(MyStats)
  469. self.stats_server.run()
  470. # checking IPv6 enabled on this platform
  471. self.ipv6_enabled = is_ipv6_enabled()
  472. def tearDown(self):
  473. if hasattr(self, "stats_httpd"):
  474. self.stats_httpd.stop()
  475. self.stats_server.shutdown()
  476. self.base.shutdown()
  477. # reset the signal handler
  478. self.sig_handler.reset()
  479. def test_init(self):
  480. server_address = get_availaddr()
  481. self.stats_httpd = MyStatsHttpd(server_address)
  482. self.assertEqual(self.stats_httpd.running, False)
  483. self.assertEqual(self.stats_httpd.poll_intval, 0.5)
  484. self.assertNotEqual(len(self.stats_httpd.httpd), 0)
  485. self.assertEqual(type(self.stats_httpd.mccs), isc.config.ModuleCCSession)
  486. self.assertEqual(type(self.stats_httpd.cc_session), isc.cc.Session)
  487. self.assertEqual(len(self.stats_httpd.config), 2)
  488. self.assertTrue('listen_on' in self.stats_httpd.config)
  489. self.assertEqual(len(self.stats_httpd.config['listen_on']), 1)
  490. self.assertTrue('address' in self.stats_httpd.config['listen_on'][0])
  491. self.assertTrue('port' in self.stats_httpd.config['listen_on'][0])
  492. self.assertTrue(server_address in set(self.stats_httpd.http_addrs))
  493. ans = send_command(
  494. isc.config.ccsession.COMMAND_GET_MODULE_SPEC,
  495. "ConfigManager", {"module_name":"StatsHttpd"})
  496. # assert StatsHttpd is added to ConfigManager
  497. self.assertNotEqual(ans, (0,{}))
  498. self.assertTrue(ans[1]['module_name'], 'StatsHttpd')
  499. def test_init_hterr(self):
  500. orig_open_httpd = stats_httpd.StatsHttpd.open_httpd
  501. def err_open_httpd(arg): raise stats_httpd.HttpServerError
  502. stats_httpd.StatsHttpd.open_httpd = err_open_httpd
  503. self.assertRaises(stats_httpd.HttpServerError, stats_httpd.StatsHttpd)
  504. ans = send_command(
  505. isc.config.ccsession.COMMAND_GET_MODULE_SPEC,
  506. "ConfigManager", {"module_name":"StatsHttpd"})
  507. # assert StatsHttpd is removed from ConfigManager
  508. self.assertEqual(ans, (0,{}))
  509. stats_httpd.StatsHttpd.open_httpd = orig_open_httpd
  510. def test_openclose_mccs(self):
  511. self.stats_httpd = MyStatsHttpd(get_availaddr())
  512. mccs = MockModuleCCSession()
  513. self.stats_httpd.mccs = mccs
  514. self.assertFalse(self.stats_httpd.mccs.stopped)
  515. self.assertFalse(self.stats_httpd.mccs.closed)
  516. self.stats_httpd.close_mccs()
  517. self.assertTrue(mccs.stopped)
  518. self.assertTrue(mccs.closed)
  519. self.assertEqual(self.stats_httpd.mccs, None)
  520. self.stats_httpd.open_mccs()
  521. self.assertIsNotNone(self.stats_httpd.mccs)
  522. self.stats_httpd.mccs = None
  523. self.assertEqual(self.stats_httpd.mccs, None)
  524. self.assertEqual(self.stats_httpd.close_mccs(), None)
  525. def test_mccs(self):
  526. self.stats_httpd = MyStatsHttpd(get_availaddr())
  527. self.assertIsNotNone(self.stats_httpd.mccs.get_socket())
  528. self.assertTrue(
  529. isinstance(self.stats_httpd.mccs.get_socket(), socket.socket))
  530. self.assertTrue(
  531. isinstance(self.stats_httpd.cc_session, isc.cc.session.Session))
  532. statistics_spec = self.stats_httpd.get_stats_spec()
  533. for mod in DUMMY_DATA:
  534. self.assertTrue(mod in statistics_spec)
  535. for cfg in statistics_spec[mod]:
  536. self.assertTrue('item_name' in cfg)
  537. self.assertTrue(cfg['item_name'] in DUMMY_DATA[mod])
  538. self.assertTrue(len(statistics_spec[mod]), len(DUMMY_DATA[mod]))
  539. self.stats_httpd.close_mccs()
  540. self.assertIsNone(self.stats_httpd.mccs)
  541. def test_httpd(self):
  542. # dual stack (addresses is ipv4 and ipv6)
  543. if self.ipv6_enabled:
  544. server_addresses = (get_availaddr('::1'), get_availaddr())
  545. self.stats_httpd = MyStatsHttpd(*server_addresses)
  546. for ht in self.stats_httpd.httpd:
  547. self.assertTrue(isinstance(ht, stats_httpd.HttpServer))
  548. self.assertTrue(ht.address_family in set([socket.AF_INET, socket.AF_INET6]))
  549. self.assertTrue(isinstance(ht.socket, socket.socket))
  550. # dual stack (address is ipv6)
  551. if self.ipv6_enabled:
  552. server_addresses = get_availaddr('::1')
  553. self.stats_httpd = MyStatsHttpd(server_addresses)
  554. for ht in self.stats_httpd.httpd:
  555. self.assertTrue(isinstance(ht, stats_httpd.HttpServer))
  556. self.assertEqual(ht.address_family, socket.AF_INET6)
  557. self.assertTrue(isinstance(ht.socket, socket.socket))
  558. # dual/single stack (address is ipv4)
  559. server_addresses = get_availaddr()
  560. self.stats_httpd = MyStatsHttpd(server_addresses)
  561. for ht in self.stats_httpd.httpd:
  562. self.assertTrue(isinstance(ht, stats_httpd.HttpServer))
  563. self.assertEqual(ht.address_family, socket.AF_INET)
  564. self.assertTrue(isinstance(ht.socket, socket.socket))
  565. # any address (IPv4)
  566. server_addresses = get_availaddr(address='0.0.0.0')
  567. self.stats_httpd = MyStatsHttpd(server_addresses)
  568. for ht in self.stats_httpd.httpd:
  569. self.assertTrue(isinstance(ht, stats_httpd.HttpServer))
  570. self.assertEqual(ht.address_family,socket.AF_INET)
  571. self.assertTrue(isinstance(ht.socket, socket.socket))
  572. # any address (IPv6)
  573. if self.ipv6_enabled:
  574. server_addresses = get_availaddr(address='::')
  575. self.stats_httpd = MyStatsHttpd(server_addresses)
  576. for ht in self.stats_httpd.httpd:
  577. self.assertTrue(isinstance(ht, stats_httpd.HttpServer))
  578. self.assertEqual(ht.address_family,socket.AF_INET6)
  579. self.assertTrue(isinstance(ht.socket, socket.socket))
  580. # existent hostname
  581. self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd,
  582. get_availaddr(address='localhost'))
  583. # nonexistent hostname
  584. self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd, ('my.host.domain', 8000))
  585. # over flow of port number
  586. self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd, ('127.0.0.1', 80000))
  587. # negative
  588. self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd, ('127.0.0.1', -8000))
  589. # alphabet
  590. self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd, ('127.0.0.1', 'ABCDE'))
  591. # Address already in use
  592. server_addresses = get_availaddr()
  593. self.stats_httpd_server = ThreadingServerManager(MyStatsHttpd, server_addresses)
  594. self.stats_httpd_server.run()
  595. self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd, server_addresses)
  596. send_shutdown("StatsHttpd")
  597. def test_running(self):
  598. self.stats_httpd_server = ThreadingServerManager(MyStatsHttpd, get_availaddr())
  599. self.stats_httpd = self.stats_httpd_server.server
  600. self.assertFalse(self.stats_httpd.running)
  601. self.stats_httpd_server.run()
  602. self.assertEqual(send_command("status", "StatsHttpd"),
  603. (0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")"))
  604. self.assertTrue(self.stats_httpd.running)
  605. self.assertEqual(send_shutdown("StatsHttpd"), (0, None))
  606. self.assertFalse(self.stats_httpd.running)
  607. self.stats_httpd_server.shutdown()
  608. # failure case
  609. self.stats_httpd = MyStatsHttpd(get_availaddr())
  610. self.stats_httpd.cc_session.close()
  611. self.assertRaises(ValueError, self.stats_httpd.start)
  612. def test_failure_with_a_select_error (self):
  613. """checks select.error is raised if the exception except
  614. errno.EINTR is raised while it's selecting"""
  615. def raise_select_except(*args):
  616. raise select.error('dummy error')
  617. orig_select = stats_httpd.select.select
  618. stats_httpd.select.select = raise_select_except
  619. self.stats_httpd = MyStatsHttpd(get_availaddr())
  620. self.assertRaises(select.error, self.stats_httpd.start)
  621. stats_httpd.select.select = orig_select
  622. def test_nofailure_with_errno_EINTR(self):
  623. """checks no exception is raised if errno.EINTR is raised
  624. while it's selecting"""
  625. def raise_select_except(*args):
  626. raise select.error(errno.EINTR)
  627. orig_select = stats_httpd.select.select
  628. stats_httpd.select.select = raise_select_except
  629. self.stats_httpd_server = ThreadingServerManager(MyStatsHttpd, get_availaddr())
  630. self.stats_httpd_server.run()
  631. self.stats_httpd_server.shutdown()
  632. stats_httpd.select.select = orig_select
  633. def test_open_template(self):
  634. self.stats_httpd = MyStatsHttpd(get_availaddr())
  635. # successful conditions
  636. tmpl = self.stats_httpd.open_template(stats_httpd.XML_TEMPLATE_LOCATION)
  637. self.assertTrue(isinstance(tmpl, string.Template))
  638. opts = dict(
  639. xml_string="<dummy></dummy>",
  640. xsl_url_path="/path/to/")
  641. lines = tmpl.substitute(opts)
  642. for n in opts:
  643. self.assertTrue(lines.find(opts[n])>0)
  644. tmpl = self.stats_httpd.open_template(stats_httpd.XSD_TEMPLATE_LOCATION)
  645. self.assertTrue(isinstance(tmpl, string.Template))
  646. opts = dict(xsd_namespace="http://host/path/to/")
  647. lines = tmpl.substitute(opts)
  648. for n in opts:
  649. self.assertTrue(lines.find(opts[n])>0)
  650. tmpl = self.stats_httpd.open_template(stats_httpd.XSL_TEMPLATE_LOCATION)
  651. self.assertTrue(isinstance(tmpl, string.Template))
  652. opts = dict(xsd_namespace="http://host/path/to/")
  653. lines = tmpl.substitute(opts)
  654. for n in opts:
  655. self.assertTrue(lines.find(opts[n])>0)
  656. # unsuccessful condition
  657. self.assertRaises(
  658. stats_httpd.StatsHttpdDataError,
  659. self.stats_httpd.open_template, '/path/to/foo/bar')
  660. def test_commands(self):
  661. self.stats_httpd = MyStatsHttpd(get_availaddr())
  662. self.assertEqual(self.stats_httpd.command_handler("status", None),
  663. isc.config.ccsession.create_answer(
  664. 0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")"))
  665. self.stats_httpd.running = True
  666. self.assertEqual(self.stats_httpd.command_handler("shutdown", None),
  667. isc.config.ccsession.create_answer(0))
  668. self.assertFalse(self.stats_httpd.running)
  669. self.assertEqual(
  670. self.stats_httpd.command_handler("__UNKNOWN_COMMAND__", None),
  671. isc.config.ccsession.create_answer(
  672. 1, "Unknown command: __UNKNOWN_COMMAND__"))
  673. def test_config(self):
  674. self.stats_httpd = MyStatsHttpd(get_availaddr())
  675. self.assertEqual(
  676. self.stats_httpd.config_handler(dict(_UNKNOWN_KEY_=None)),
  677. isc.config.ccsession.create_answer(
  678. 1, "unknown item _UNKNOWN_KEY_"))
  679. addresses = get_availaddr()
  680. self.assertEqual(
  681. self.stats_httpd.config_handler(
  682. dict(listen_on=[dict(address=addresses[0],port=addresses[1])])),
  683. isc.config.ccsession.create_answer(0))
  684. self.assertTrue("listen_on" in self.stats_httpd.config)
  685. for addr in self.stats_httpd.config["listen_on"]:
  686. self.assertTrue("address" in addr)
  687. self.assertTrue("port" in addr)
  688. self.assertTrue(addr["address"] == addresses[0])
  689. self.assertTrue(addr["port"] == addresses[1])
  690. if self.ipv6_enabled:
  691. addresses = get_availaddr("::1")
  692. self.assertEqual(
  693. self.stats_httpd.config_handler(
  694. dict(listen_on=[dict(address=addresses[0],port=addresses[1])])),
  695. isc.config.ccsession.create_answer(0))
  696. self.assertTrue("listen_on" in self.stats_httpd.config)
  697. for addr in self.stats_httpd.config["listen_on"]:
  698. self.assertTrue("address" in addr)
  699. self.assertTrue("port" in addr)
  700. self.assertTrue(addr["address"] == addresses[0])
  701. self.assertTrue(addr["port"] == addresses[1])
  702. addresses = get_availaddr()
  703. self.assertEqual(
  704. self.stats_httpd.config_handler(
  705. dict(listen_on=[dict(address=addresses[0],port=addresses[1])])),
  706. isc.config.ccsession.create_answer(0))
  707. self.assertTrue("listen_on" in self.stats_httpd.config)
  708. for addr in self.stats_httpd.config["listen_on"]:
  709. self.assertTrue("address" in addr)
  710. self.assertTrue("port" in addr)
  711. self.assertTrue(addr["address"] == addresses[0])
  712. self.assertTrue(addr["port"] == addresses[1])
  713. (ret, arg) = isc.config.ccsession.parse_answer(
  714. self.stats_httpd.config_handler(
  715. dict(listen_on=[dict(address="1.2.3.4",port=543210)]))
  716. )
  717. self.assertEqual(ret, 1)
  718. def test_xml_handler(self):
  719. self.stats_httpd = MyStatsHttpd(get_availaddr())
  720. module_name = 'Dummy'
  721. stats_spec = \
  722. { module_name :
  723. [{
  724. "item_name": "foo",
  725. "item_type": "string",
  726. "item_optional": False,
  727. "item_default": "bar",
  728. "item_description": "foo is bar",
  729. "item_title": "Foo"
  730. },
  731. {
  732. "item_name": "foo2",
  733. "item_type": "list",
  734. "item_optional": False,
  735. "item_default": [
  736. {
  737. "zonename" : "test1",
  738. "queries.udp" : 1,
  739. "queries.tcp" : 2
  740. },
  741. {
  742. "zonename" : "test2",
  743. "queries.udp" : 3,
  744. "queries.tcp" : 4
  745. }
  746. ],
  747. "item_title": "Foo bar",
  748. "item_description": "Foo bar",
  749. "list_item_spec": {
  750. "item_name": "foo2-1",
  751. "item_type": "map",
  752. "item_optional": False,
  753. "item_default": {},
  754. "map_item_spec": [
  755. {
  756. "item_name": "foo2-1-1",
  757. "item_type": "string",
  758. "item_optional": False,
  759. "item_default": "",
  760. "item_title": "Foo2 1 1",
  761. "item_description": "Foo bar"
  762. },
  763. {
  764. "item_name": "foo2-1-2",
  765. "item_type": "integer",
  766. "item_optional": False,
  767. "item_default": 0,
  768. "item_title": "Foo2 1 2",
  769. "item_description": "Foo bar"
  770. },
  771. {
  772. "item_name": "foo2-1-3",
  773. "item_type": "integer",
  774. "item_optional": False,
  775. "item_default": 0,
  776. "item_title": "Foo2 1 3",
  777. "item_description": "Foo bar"
  778. }
  779. ]
  780. }
  781. }]
  782. }
  783. stats_data = \
  784. { module_name : { 'foo':'bar',
  785. 'foo2': [
  786. {
  787. "foo2-1-1" : "bar1",
  788. "foo2-1-2" : 10,
  789. "foo2-1-3" : 9
  790. },
  791. {
  792. "foo2-1-1" : "bar2",
  793. "foo2-1-2" : 8,
  794. "foo2-1-3" : 7
  795. }
  796. ] } }
  797. self.stats_httpd.get_stats_spec = lambda x,y: stats_spec
  798. self.stats_httpd.get_stats_data = lambda x,y: stats_data
  799. xml_string = self.stats_httpd.xml_handler()
  800. stats_xml = xml.etree.ElementTree.fromstring(xml_string)
  801. schema_loc = '{%s}schemaLocation' % XMLNS_XSI
  802. self.assertEqual(stats_xml.attrib[schema_loc],
  803. stats_httpd.XML_ROOT_ATTRIB['xsi:schemaLocation'])
  804. stats_data = stats_data[module_name]
  805. stats_spec = stats_spec[module_name]
  806. names = stats_httpd.item_name_list(stats_data, '')
  807. for i in range(0, len(names)):
  808. self.assertEqual('%s/%s' % (module_name, names[i]), stats_xml[i].attrib['identifier'])
  809. value = isc.cc.data.find(stats_data, names[i])
  810. if type(value) is int:
  811. value = str(value)
  812. if type(value) is dict or type(value) is list:
  813. self.assertFalse('value' in stats_xml[i].attrib)
  814. else:
  815. self.assertEqual(value, stats_xml[i].attrib['value'])
  816. self.assertEqual(module_name, stats_xml[i].attrib['owner'])
  817. self.assertEqual(urllib.parse.quote('%s/%s/%s' % (stats_httpd.XML_URL_PATH,
  818. module_name, names[i])),
  819. stats_xml[i].attrib['uri'])
  820. spec = isc.config.find_spec_part(stats_spec, names[i])
  821. self.assertEqual(spec['item_name'], stats_xml[i].attrib['name'])
  822. self.assertEqual(spec['item_type'], stats_xml[i].attrib['type'])
  823. self.assertEqual(spec['item_description'], stats_xml[i].attrib['description'])
  824. self.assertEqual(spec['item_title'], stats_xml[i].attrib['title'])
  825. self.assertEqual(str(spec['item_optional']).lower(), stats_xml[i].attrib['optional'])
  826. default = spec['item_default']
  827. if type(default) is int:
  828. default = str(default)
  829. if type(default) is dict or type(default) is list:
  830. self.assertFalse('default' in stats_xml[i].attrib)
  831. else:
  832. self.assertEqual(default, stats_xml[i].attrib['default'])
  833. self.assertFalse('item_format' in spec)
  834. self.assertFalse('format' in stats_xml[i].attrib)
  835. def test_xsd_handler(self):
  836. self.stats_httpd = MyStatsHttpd(get_availaddr())
  837. xsd_string = self.stats_httpd.xsd_handler()
  838. stats_xsd = xml.etree.ElementTree.fromstring(xsd_string)
  839. ns = '{%s}' % XMLNS_XSD
  840. stats_xsd = stats_xsd[1].find('%scomplexType/%ssequence/%selement' % ((ns,)*3))
  841. self.assertEqual('item', stats_xsd.attrib['name'])
  842. stats_xsd = stats_xsd.find('%scomplexType' % ns)
  843. type_types = ('boolean', 'integer', 'real', 'string', 'map', \
  844. 'list', 'named_set', 'any')
  845. attribs = [('identifier', 'string', 'required'),
  846. ('value', 'string', 'optional'),
  847. ('owner', 'string', 'required'),
  848. ('uri', 'anyURI', 'required'),
  849. ('name', 'string', 'required'),
  850. ('type', type_types, 'required'),
  851. ('description', 'string', 'optional'),
  852. ('title', 'string', 'optional'),
  853. ('optional', 'boolean', 'optional'),
  854. ('default', 'string', 'optional'),
  855. ('format', 'string', 'optional')]
  856. for i in range(0, len(attribs)):
  857. self.assertEqual(attribs[i][0], stats_xsd[i].attrib['name'])
  858. if attribs[i][0] == 'type':
  859. stats_xsd_types = \
  860. stats_xsd[i].find('%ssimpleType/%srestriction' % \
  861. ((ns,)*2))
  862. for j in range(0, len(attribs[i][1])):
  863. self.assertEqual(attribs[i][1][j], \
  864. stats_xsd_types[j].attrib['value'])
  865. else:
  866. self.assertEqual(attribs[i][1], stats_xsd[i].attrib['type'])
  867. self.assertEqual(attribs[i][2], stats_xsd[i].attrib['use'])
  868. def test_xsl_handler(self):
  869. self.stats_httpd = MyStatsHttpd(get_availaddr())
  870. xsl_string = self.stats_httpd.xsl_handler()
  871. stats_xsl = xml.etree.ElementTree.fromstring(xsl_string)
  872. nst = '{%s}' % XMLNS_XSL
  873. nsx = '{%s}' % XMLNS_XHTML
  874. self.assertEqual("bind10:statistics", stats_xsl[2].attrib['match'])
  875. stats_xsl = stats_xsl[2].find('%stable' % nsx)
  876. self.assertEqual('item', stats_xsl[1].attrib['select'])
  877. stats_xsl = stats_xsl[1].find('%str' % nsx)
  878. self.assertEqual('@uri', stats_xsl[0].find(
  879. '%selement/%sattribute/%svalue-of' % ((nst,)*3)).attrib['select'])
  880. self.assertEqual('@identifier', stats_xsl[0].find(
  881. '%selement/%svalue-of' % ((nst,)*2)).attrib['select'])
  882. self.assertEqual('@value', stats_xsl[1].find('%sif' % nst).attrib['test'])
  883. self.assertEqual('@value', stats_xsl[1].find('%sif/%svalue-of' % ((nst,)*2)).attrib['select'])
  884. self.assertEqual('@description', stats_xsl[2].find('%sif' % nst).attrib['test'])
  885. self.assertEqual('@description', stats_xsl[2].find('%sif/%svalue-of' % ((nst,)*2)).attrib['select'])
  886. def test_for_without_B10_FROM_SOURCE(self):
  887. # just lets it go through the code without B10_FROM_SOURCE env
  888. # variable
  889. if "B10_FROM_SOURCE" in os.environ:
  890. tmppath = os.environ["B10_FROM_SOURCE"]
  891. os.environ.pop("B10_FROM_SOURCE")
  892. imp.reload(stats_httpd)
  893. os.environ["B10_FROM_SOURCE"] = tmppath
  894. imp.reload(stats_httpd)
  895. if __name__ == "__main__":
  896. unittest.main()