stats_httpd.py.in 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. #!@PYTHON@
  2. # Copyright (C) 2011 Internet Systems Consortium.
  3. #
  4. # Permission to use, copy, modify, and distribute this software for any
  5. # purpose with or without fee is hereby granted, provided that the above
  6. # copyright notice and this permission notice appear in all copies.
  7. #
  8. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  9. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  10. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  11. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  12. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  13. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  14. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  15. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  16. import sys; sys.path.append ('@@PYTHONPATH@@')
  17. import os
  18. import time
  19. import errno
  20. import select
  21. from optparse import OptionParser, OptionValueError
  22. import http.server
  23. import socket
  24. import string
  25. import xml.etree.ElementTree
  26. import isc.cc
  27. import isc.config
  28. import isc.util.process
  29. # If B10_FROM_SOURCE is set in the environment, we use data files
  30. # from a directory relative to that, otherwise we use the ones
  31. # installed on the system
  32. if "B10_FROM_SOURCE" in os.environ:
  33. BASE_LOCATION = os.environ["B10_FROM_SOURCE"]
  34. else:
  35. PREFIX = "@prefix@"
  36. DATAROOTDIR = "@datarootdir@"
  37. BASE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@"
  38. BASE_LOCATION = BASE_LOCATION.replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
  39. SPECFILE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd.spec"
  40. STATS_SPECFILE_LOCATION = BASE_LOCATION + os.sep + "stats.spec"
  41. XML_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xml.tpl"
  42. XSD_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xsd.tpl"
  43. XSL_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xsl.tpl"
  44. # These variables are paths part of URL.
  45. # eg. "http://${address}" + XXX_URL_PATH
  46. XML_URL_PATH = '/bind10/statistics/xml'
  47. XSD_URL_PATH = '/bind10/statistics/xsd'
  48. XSL_URL_PATH = '/bind10/statistics/xsl'
  49. # TODO: This should be considered later.
  50. XSD_NAMESPACE = 'http://bind10.isc.org' + XSD_URL_PATH
  51. DEFAULT_CONFIG = dict(listen_on=[('127.0.0.1', 8000)])
  52. # Assign this process name
  53. isc.util.process.rename()
  54. class HttpHandler(http.server.BaseHTTPRequestHandler):
  55. """HTTP handler class for HttpServer class. The class inhrits the super
  56. class http.server.BaseHTTPRequestHandler. It implemets do_GET()
  57. and do_HEAD() and orverrides log_message()"""
  58. def do_GET(self):
  59. body = self.send_head()
  60. if body is not None:
  61. self.wfile.write(body.encode())
  62. def do_HEAD(self):
  63. self.send_head()
  64. def send_head(self):
  65. assert isinstance(self.server, HttpServer)
  66. try:
  67. if self.path == XML_URL_PATH:
  68. body = self.server.xml_handler()
  69. elif self.path == XSD_URL_PATH:
  70. body = self.server.xsd_handler()
  71. elif self.path == XSL_URL_PATH:
  72. body = self.server.xsl_handler()
  73. else:
  74. if 'Host' in self.headers.keys():
  75. # redirect to XML URL
  76. self.send_response(302)
  77. self.send_header(
  78. "Location",
  79. "http://" + self.headers.get('Host') + XML_URL_PATH)
  80. self.end_headers()
  81. return None
  82. else:
  83. # Couldn't find HOST
  84. self.send_error(404)
  85. return None
  86. except StatsHttpdError as err:
  87. self.send_error(500)
  88. if self.server.verbose:
  89. self.server.log_writer(
  90. "[b10-stats-httpd] %s\n" % err)
  91. return None
  92. else:
  93. self.send_response(200)
  94. self.send_header("Content-type", "text/xml")
  95. self.send_header("Content-Length", len(body))
  96. self.end_headers()
  97. return body
  98. def log_message(self, format, *args):
  99. """Change the default log format"""
  100. assert isinstance(self.server, HttpServer)
  101. if self.server.verbose:
  102. self.server.log_writer(
  103. "[b10-stats-httpd] %s - - [%s] %s\n" %
  104. (self.address_string(),
  105. self.log_date_time_string(),
  106. format%args))
  107. class HttpServerError(Exception):
  108. """Exception class for HttpServer class. It is intended to be
  109. passed from the HttpServer object to the StatsHttpd object."""
  110. pass
  111. class HttpServer(http.server.HTTPServer):
  112. """HTTP Server class. The class inherits the super
  113. http.server.HTTPServer. Some parameters are specified as
  114. arguments, which are xml_handler, xsd_handler, xsl_handler, and
  115. log_writer. These all are parameters which the StatsHttpd object
  116. has. The handler parameters are references of functions which
  117. return body of each document. The last parameter log_writer is
  118. reference of writer function to just write to
  119. sys.stderr.write. They are intended to be referred by HttpHandler
  120. object."""
  121. def __init__(self, server_address, handler,
  122. xml_handler, xsd_handler, xsl_handler, log_writer, verbose=False):
  123. self.server_address = server_address
  124. self.xml_handler = xml_handler
  125. self.xsd_handler = xsd_handler
  126. self.xsl_handler = xsl_handler
  127. self.log_writer = log_writer
  128. self.verbose = verbose
  129. http.server.HTTPServer.__init__(self, server_address, handler)
  130. class StatsHttpdError(Exception):
  131. """Exception class for StatsHttpd class. It is intended to be
  132. thrown from the the StatsHttpd object to the HttpHandler object or
  133. main routine."""
  134. pass
  135. class StatsHttpd:
  136. """The main class of HTTP server of HTTP/XML interface for
  137. statistics module. It handles HTTP requests, and command channel
  138. and config channel CC session. It uses select.select function
  139. while waiting for clients requests."""
  140. def __init__(self, verbose=False):
  141. self.verbose = verbose
  142. self.running = False
  143. self.poll_intval = 0.5
  144. self.write_log = sys.stderr.write
  145. self.mccs = None
  146. self.httpd = []
  147. self.open_mccs()
  148. self.load_config()
  149. self.load_templates()
  150. self.open_httpd()
  151. def open_mccs(self):
  152. """Opens a ModuleCCSession object"""
  153. # create ModuleCCSession
  154. if self.verbose:
  155. self.write_log("[b10-stats-httpd] Starting CC Session\n")
  156. self.mccs = isc.config.ModuleCCSession(
  157. SPECFILE_LOCATION, self.config_handler, self.command_handler)
  158. self.cc_session = self.mccs._session
  159. # read spec file of stats module and subscribe 'Stats'
  160. self.stats_module_spec = isc.config.module_spec_from_file(STATS_SPECFILE_LOCATION)
  161. self.stats_config_spec = self.stats_module_spec.get_config_spec()
  162. self.stats_module_name = self.stats_module_spec.get_module_name()
  163. if self.verbose:
  164. self.write_log("[b10-stats-httpd] Starts to subscribe stats module\n")
  165. self.cc_session.group_subscribe(self.stats_module_name, "*")
  166. def close_mccs(self):
  167. """Closes a ModuleCCSession object"""
  168. if self.mccs is None:
  169. return
  170. if self.verbose:
  171. self.write_log("[b10-stats-httpd] Closing CC Session\n")
  172. try:
  173. self.cc_session.group_unsubscribe(self.stats_module_name, "*")
  174. except isc.cc.session.SessionError as se:
  175. if self.verbose:
  176. self.write_log("[b10-stats-httpd] Failed to unsubscribe stats module\n")
  177. self.mccs.close()
  178. self.mccs = None
  179. def load_config(self, new_config={}):
  180. """Loads configuration from spec file or new configuration
  181. from the config manager"""
  182. # load config
  183. if len(new_config) > 0:
  184. assert type(self.config) is dict
  185. assert type(new_config) is dict
  186. self.config.update(new_config)
  187. else:
  188. self.config = DEFAULT_CONFIG
  189. self.config.update(
  190. dict([
  191. (itm['item_name'], self.mccs.get_value(itm['item_name'])[0])
  192. for itm in self.mccs.get_module_spec().get_config_spec()
  193. ])
  194. )
  195. assert 'listen_on' in self.config
  196. assert type(self.config['listen_on']) is list
  197. # remove duplicated element
  198. self.http_addrs = list(
  199. set([ (cf['address'], cf['port']) for cf in self.config['listen_on'] ])
  200. )
  201. def open_httpd(self):
  202. """Opens sockets for HTTP. Iterating each HTTP address to be
  203. configured in spec file"""
  204. assert type(self.http_addrs) is list
  205. for addr in self.http_addrs:
  206. self.httpd.append(self._open_httpd(addr))
  207. def _open_httpd(self, server_address, address_family=None):
  208. assert type(server_address) is tuple
  209. try:
  210. # try IPv6 at first
  211. if address_family is not None:
  212. HttpServer.address_family = address_family
  213. elif socket.has_ipv6:
  214. HttpServer.address_family = socket.AF_INET6
  215. httpd = HttpServer(
  216. server_address, HttpHandler,
  217. self.xml_handler, self.xsd_handler, self.xsl_handler,
  218. self.write_log, self.verbose)
  219. except (socket.gaierror, socket.error,
  220. OverflowError, TypeError) as err:
  221. # try IPv4 next
  222. if HttpServer.address_family == socket.AF_INET6:
  223. httpd = self._open_httpd(server_address, socket.AF_INET)
  224. else:
  225. raise HttpServerError(
  226. "Invalid address %s, port %s: %s: %s" %
  227. (server_address[0], server_address[1],
  228. err.__class__.__name__, err))
  229. else:
  230. if self.verbose:
  231. self.write_log(
  232. "[b10-stats-httpd] Started on address %s, port %s\n" %
  233. server_address)
  234. assert isinstance(httpd, HttpServer)
  235. return httpd
  236. def close_httpd(self):
  237. """Closes sockets for HTTP"""
  238. if len(self.httpd) == 0:
  239. return
  240. for ht in self.httpd:
  241. assert type(ht.server_address) is tuple
  242. if self.verbose:
  243. self.write_log(
  244. "[b10-stats-httpd] Closing address %s, port %s\n" %
  245. (ht.server_address[0], ht.server_address[1])
  246. )
  247. ht.server_close()
  248. self.httpd = []
  249. def start(self):
  250. """Starts StatsHttpd objects to run. Waiting for client
  251. requests by using select.select functions"""
  252. self.mccs.start()
  253. self.running = True
  254. while self.running:
  255. try:
  256. (rfd, wfd, xfd) = select.select(
  257. self.get_sockets(), [], [], self.poll_intval)
  258. except select.error as err:
  259. if err.args[0] == errno.EINTR:
  260. (rfd, wfd, xfd) = ([], [], [])
  261. else:
  262. raise select.error(err)
  263. for fd in rfd + xfd:
  264. if fd == self.mccs.get_socket():
  265. self.mccs.check_command(nonblock=False)
  266. continue
  267. for ht in self.httpd:
  268. if fd == ht.socket:
  269. ht.handle_request()
  270. break
  271. self.stop()
  272. def stop(self):
  273. """Stops the running StatsHttpd objects. Closes CC session and
  274. HTTP handling sockets"""
  275. if self.verbose:
  276. self.write_log("[b10-stats-httpd] Shutting down\n")
  277. self.close_httpd()
  278. self.close_mccs()
  279. def get_sockets(self):
  280. """Returns sockets to select.select"""
  281. sockets = []
  282. if self.mccs is not None:
  283. sockets.append(self.mccs.get_socket())
  284. if len(self.httpd) > 0:
  285. for ht in self.httpd:
  286. sockets.append(ht.socket)
  287. return sockets
  288. def config_handler(self, new_config):
  289. """Config handler for the ModuleCCSession object. It resets
  290. addresses and ports to listen HTTP requests on."""
  291. assert type(new_config) is dict
  292. if self.verbose:
  293. self.write_log("[b10-stats-httpd] Loading config : %s\n" % str(new_config))
  294. for key in new_config.keys():
  295. if key not in DEFAULT_CONFIG:
  296. if self.verbose:
  297. self.write_log(
  298. "[b10-stats-httpd] Unknown known config: %s" % key)
  299. return isc.config.ccsession.create_answer(
  300. 1, "Unknown known config: %s" % key)
  301. # backup old config
  302. old_config = self.config.copy()
  303. self.close_httpd()
  304. self.load_config(new_config)
  305. try:
  306. self.open_httpd()
  307. except HttpServerError as err:
  308. if self.verbose:
  309. self.write_log("[b10-stats-httpd] %s\n" % err)
  310. self.write_log("[b10-stats-httpd] Restoring old config\n")
  311. # restore old config
  312. self.config_handler(old_config)
  313. return isc.config.ccsession.create_answer(
  314. 1, "[b10-stats-httpd] %s" % err)
  315. else:
  316. return isc.config.ccsession.create_answer(0)
  317. def command_handler(self, command, args):
  318. """Command handler for the ModuleCCSesson object. It handles
  319. "status" and "shutdown" commands."""
  320. if command == "status":
  321. if self.verbose:
  322. self.write_log("[b10-stats-httpd] Received 'status' command\n")
  323. return isc.config.ccsession.create_answer(
  324. 0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")")
  325. elif command == "shutdown":
  326. if self.verbose:
  327. self.write_log("[b10-stats-httpd] Received 'shutdown' command\n")
  328. self.running = False
  329. return isc.config.ccsession.create_answer(
  330. 0, "Stats Httpd is shutting down.")
  331. else:
  332. if self.verbose:
  333. self.write_log("[b10-stats-httpd] Received unknown command\n")
  334. return isc.config.ccsession.create_answer(
  335. 1, "Unknown command: " + str(command))
  336. def get_stats_data(self):
  337. """Requests statistics data to the Stats daemon and returns
  338. the data which obtains from it"""
  339. try:
  340. seq = self.cc_session.group_sendmsg(
  341. isc.config.ccsession.create_command('show'),
  342. self.stats_module_name)
  343. (answer, env) = self.cc_session.group_recvmsg(False, seq)
  344. if answer:
  345. (rcode, value) = isc.config.ccsession.parse_answer(answer)
  346. self.cc_session.group_reply(
  347. env, isc.config.ccsession.create_answer(0))
  348. except (isc.cc.session.SessionTimeout,
  349. isc.cc.session.SessionError) as err:
  350. raise StatsHttpdError("%s: %s" %
  351. (err.__class__.__name__, err))
  352. else:
  353. if rcode == 0:
  354. return value
  355. else:
  356. raise StatsHttpdError("Stats module: %s" % str(value))
  357. def get_stats_spec(self):
  358. """Just returns spec data"""
  359. return self.stats_config_spec
  360. def load_templates(self):
  361. """Setup the bodies of XSD and XSL documents to be responds to
  362. HTTP clients. Before that it also creates XML tag structures by
  363. using xml.etree.ElementTree.Element class and substitutes
  364. concrete strings with parameters embed in the string.Template
  365. object."""
  366. # for XSD
  367. xsd_root = xml.etree.ElementTree.Element("all") # started with "all" tag
  368. for item in self.get_stats_spec():
  369. element = xml.etree.ElementTree.Element(
  370. "element",
  371. dict( name=item["item_name"],
  372. type=item["item_type"] if item["item_type"].lower() != 'real' else 'float',
  373. minOccurs="1",
  374. maxOccurs="1" ),
  375. )
  376. annotation = xml.etree.ElementTree.Element("annotation")
  377. appinfo = xml.etree.ElementTree.Element("appinfo")
  378. documentation = xml.etree.ElementTree.Element("documentation")
  379. appinfo.text = item["item_title"]
  380. documentation.text = item["item_description"]
  381. annotation.append(appinfo)
  382. annotation.append(documentation)
  383. element.append(annotation)
  384. xsd_root.append(element)
  385. xsd_string = xml.etree.ElementTree.tostring(xsd_root)
  386. self.xsd_body = self.open_template(XSD_TEMPLATE_LOCATION).substitute(
  387. xsd_string=xsd_string,
  388. xsd_namespace=XSD_NAMESPACE
  389. )
  390. assert self.xsd_body is not None
  391. # for XSL
  392. xsd_root = xml.etree.ElementTree.Element(
  393. "xsl:template",
  394. dict(match="*")) # started with xml:template tag
  395. for item in self.get_stats_spec():
  396. tr = xml.etree.ElementTree.Element("tr")
  397. td1 = xml.etree.ElementTree.Element(
  398. "td", { "class" : "title",
  399. "title" : item["item_description"] })
  400. td1.text = item["item_title"]
  401. td2 = xml.etree.ElementTree.Element("td")
  402. xsl_valueof = xml.etree.ElementTree.Element(
  403. "xsl:value-of",
  404. dict(select=item["item_name"]))
  405. td2.append(xsl_valueof)
  406. tr.append(td1)
  407. tr.append(td2)
  408. xsd_root.append(tr)
  409. xsl_string = xml.etree.ElementTree.tostring(xsd_root)
  410. self.xsl_body = self.open_template(XSL_TEMPLATE_LOCATION).substitute(
  411. xsl_string=xsl_string,
  412. xsd_namespace=XSD_NAMESPACE)
  413. assert self.xsl_body is not None
  414. def xml_handler(self):
  415. """Handler which requests to Stats daemon to obtain statistics
  416. data and returns the body of XML document"""
  417. xml_list=[]
  418. for (k, v) in self.get_stats_data().items():
  419. (k, v) = (str(k), str(v))
  420. elem = xml.etree.ElementTree.Element(k)
  421. elem.text = v
  422. xml_list.append(
  423. xml.etree.ElementTree.tostring(elem))
  424. assert len(xml_list) > 0
  425. xml_string = "".join(xml_list)
  426. self.xml_body = self.open_template(XML_TEMPLATE_LOCATION).substitute(
  427. xml_string=xml_string,
  428. xsd_namespace=XSD_NAMESPACE,
  429. xsd_url_path=XSD_URL_PATH,
  430. xsl_url_path=XSL_URL_PATH)
  431. assert self.xml_body is not None
  432. return self.xml_body
  433. def xsd_handler(self):
  434. """Handler which just returns the body of XSD document"""
  435. return self.xsd_body
  436. def xsl_handler(self):
  437. """Handler which just returns the body of XSL document"""
  438. return self.xsl_body
  439. def open_template(self, file_name):
  440. """It opens a template file, and it loads all lines to a
  441. string variable and returns string. Template object includes
  442. the variable. Limitation of a file size isn't needed there."""
  443. lines = "".join(
  444. open(file_name, 'r').readlines())
  445. assert lines is not None
  446. return string.Template(lines)
  447. if __name__ == "__main__":
  448. try:
  449. parser = OptionParser()
  450. parser.add_option(
  451. "-v", "--verbose", dest="verbose", action="store_true",
  452. help="display more about what is going on")
  453. (options, args) = parser.parse_args()
  454. stats_httpd = StatsHttpd(verbose=options.verbose)
  455. stats_httpd.start()
  456. except OptionValueError:
  457. sys.stderr.write("[b10-stats-httpd] Error parsing options\n")
  458. except isc.cc.session.SessionError as se:
  459. sys.stderr.write("[b10-stats-httpd] Error creating module, "
  460. + "is the command channel daemon running?\n")
  461. except HttpServerError as hse:
  462. sys.stderr.write("[b10-stats-httpd] %s\n" % hse)
  463. except KeyboardInterrupt as kie:
  464. sys.stderr.write("[b10-stats-httpd] Interrupted, exiting\n")