stats_httpd.py.in 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
  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. """
  17. A standalone HTTP server for HTTP/XML interface of statistics in BIND 10
  18. """
  19. import sys; sys.path.append ('@@PYTHONPATH@@')
  20. import os
  21. import time
  22. import errno
  23. import select
  24. from optparse import OptionParser, OptionValueError
  25. import http.server
  26. import socket
  27. import string
  28. import xml.etree.ElementTree
  29. import urllib.parse
  30. import isc.cc
  31. import isc.config
  32. import isc.util.process
  33. import isc.log
  34. from isc.log_messages.stats_httpd_messages import *
  35. isc.log.init("b10-stats-httpd")
  36. logger = isc.log.Logger("stats-httpd")
  37. # Some constants for debug levels.
  38. DBG_STATHTTPD_INIT = logger.DBGLVL_START_SHUT
  39. DBG_STATHTTPD_MESSAGING = logger.DBGLVL_COMMAND
  40. # If B10_FROM_SOURCE is set in the environment, we use data files
  41. # from a directory relative to that, otherwise we use the ones
  42. # installed on the system
  43. if "B10_FROM_SOURCE" in os.environ:
  44. BASE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + \
  45. "src" + os.sep + "bin" + os.sep + "stats"
  46. else:
  47. PREFIX = "@prefix@"
  48. DATAROOTDIR = "@datarootdir@"
  49. BASE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@"
  50. BASE_LOCATION = BASE_LOCATION.replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
  51. SPECFILE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd.spec"
  52. XML_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xml.tpl"
  53. XSD_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xsd.tpl"
  54. XSL_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xsl.tpl"
  55. # These variables are paths part of URL.
  56. # eg. "http://${address}" + XXX_URL_PATH
  57. XML_URL_PATH = '/bind10/statistics/xml'
  58. XSD_URL_PATH = '/bind10/statistics/xsd'
  59. XSL_URL_PATH = '/bind10/statistics/xsl'
  60. # TODO: This should be considered later.
  61. XSD_NAMESPACE = 'http://bind10.isc.org/bind10'
  62. # Assign this process name
  63. isc.util.process.rename()
  64. class HttpHandler(http.server.BaseHTTPRequestHandler):
  65. """HTTP handler class for HttpServer class. The class inhrits the super
  66. class http.server.BaseHTTPRequestHandler. It implemets do_GET()
  67. and do_HEAD() and orverrides log_message()"""
  68. def do_GET(self):
  69. body = self.send_head()
  70. if body is not None:
  71. self.wfile.write(body.encode())
  72. def do_HEAD(self):
  73. self.send_head()
  74. def send_head(self):
  75. try:
  76. req_path = self.path
  77. req_path = urllib.parse.urlsplit(req_path).path
  78. req_path = urllib.parse.unquote(req_path)
  79. req_path = os.path.normpath(req_path)
  80. path_dirs = req_path.split('/')
  81. path_dirs = [ d for d in filter(None, path_dirs) ]
  82. req_path = '/'+"/".join(path_dirs)
  83. module_name = None
  84. item_name = None
  85. # in case of /bind10/statistics/xxx/YYY/zzz
  86. if len(path_dirs) >= 5:
  87. item_name = path_dirs[4]
  88. # in case of /bind10/statistics/xxx/YYY ...
  89. if len(path_dirs) >= 4:
  90. module_name = path_dirs[3]
  91. if req_path == '/'.join([XML_URL_PATH] + path_dirs[3:5]):
  92. body = self.server.xml_handler(module_name, item_name)
  93. elif req_path == '/'.join([XSD_URL_PATH] + path_dirs[3:5]):
  94. body = self.server.xsd_handler(module_name, item_name)
  95. elif req_path == '/'.join([XSL_URL_PATH] + path_dirs[3:5]):
  96. body = self.server.xsl_handler(module_name, item_name)
  97. else:
  98. if req_path == '/' and 'Host' in self.headers.keys():
  99. # redirect to XML URL only when requested with '/'
  100. self.send_response(302)
  101. self.send_header(
  102. "Location",
  103. "http://" + self.headers.get('Host') + XML_URL_PATH)
  104. self.end_headers()
  105. return None
  106. else:
  107. # Couldn't find HOST
  108. self.send_error(404)
  109. return None
  110. except StatsHttpdDataError as err:
  111. # Couldn't find neither specified module name nor
  112. # specified item name
  113. self.send_error(404)
  114. logger.error(STATHTTPD_SERVER_DATAERROR, err)
  115. return None
  116. except StatsHttpdError as err:
  117. self.send_error(500)
  118. logger.error(STATHTTPD_SERVER_ERROR, err)
  119. return None
  120. else:
  121. self.send_response(200)
  122. self.send_header("Content-type", "text/xml")
  123. self.send_header("Content-Length", len(body))
  124. self.end_headers()
  125. return body
  126. class HttpServerError(Exception):
  127. """Exception class for HttpServer class. It is intended to be
  128. passed from the HttpServer object to the StatsHttpd object."""
  129. pass
  130. class HttpServer(http.server.HTTPServer):
  131. """HTTP Server class. The class inherits the super
  132. http.server.HTTPServer. Some parameters are specified as
  133. arguments, which are xml_handler, xsd_handler, xsl_handler, and
  134. log_writer. These all are parameters which the StatsHttpd object
  135. has. The handler parameters are references of functions which
  136. return body of each document. The last parameter log_writer is
  137. reference of writer function to just write to
  138. sys.stderr.write. They are intended to be referred by HttpHandler
  139. object."""
  140. def __init__(self, server_address, handler,
  141. xml_handler, xsd_handler, xsl_handler, log_writer):
  142. self.server_address = server_address
  143. self.xml_handler = xml_handler
  144. self.xsd_handler = xsd_handler
  145. self.xsl_handler = xsl_handler
  146. self.log_writer = log_writer
  147. http.server.HTTPServer.__init__(self, server_address, handler)
  148. class StatsHttpdError(Exception):
  149. """Exception class for StatsHttpd class. It is intended to be
  150. thrown from the the StatsHttpd object to the HttpHandler object or
  151. main routine."""
  152. pass
  153. class StatsHttpdDataError(Exception):
  154. """Exception class for StatsHttpd class. The reason seems to be
  155. due to the data. It is intended to be thrown from the the
  156. StatsHttpd object to the HttpHandler object or main routine."""
  157. pass
  158. class StatsHttpd:
  159. """The main class of HTTP server of HTTP/XML interface for
  160. statistics module. It handles HTTP requests, and command channel
  161. and config channel CC session. It uses select.select function
  162. while waiting for clients requests."""
  163. def __init__(self):
  164. self.running = False
  165. self.poll_intval = 0.5
  166. self.write_log = sys.stderr.write
  167. self.mccs = None
  168. self.httpd = []
  169. self.open_mccs()
  170. self.config = {}
  171. self.load_config()
  172. self.http_addrs = []
  173. self.mccs.start()
  174. self.open_httpd()
  175. def open_mccs(self):
  176. """Opens a ModuleCCSession object"""
  177. # create ModuleCCSession
  178. logger.debug(DBG_STATHTTPD_INIT, STATHTTPD_STARTING_CC_SESSION)
  179. self.mccs = isc.config.ModuleCCSession(
  180. SPECFILE_LOCATION, self.config_handler, self.command_handler)
  181. self.cc_session = self.mccs._session
  182. def close_mccs(self):
  183. """Closes a ModuleCCSession object"""
  184. if self.mccs is None:
  185. return
  186. self.mccs.send_stopping()
  187. logger.debug(DBG_STATHTTPD_INIT, STATHTTPD_CLOSING_CC_SESSION)
  188. self.mccs.close()
  189. self.mccs = None
  190. def load_config(self, new_config={}):
  191. """Loads configuration from spec file or new configuration
  192. from the config manager"""
  193. # load config
  194. if len(self.config) == 0:
  195. self.config = dict([
  196. (itm['item_name'], self.mccs.get_value(itm['item_name'])[0])
  197. for itm in self.mccs.get_module_spec().get_config_spec()
  198. ])
  199. self.config.update(new_config)
  200. # set addresses and ports for HTTP
  201. addrs = []
  202. if 'listen_on' in self.config:
  203. for cf in self.config['listen_on']:
  204. if 'address' in cf and 'port' in cf:
  205. addrs.append((cf['address'], cf['port']))
  206. self.http_addrs = addrs
  207. def open_httpd(self):
  208. """Opens sockets for HTTP. Iterating each HTTP address to be
  209. configured in spec file"""
  210. for addr in self.http_addrs:
  211. self.httpd.append(self._open_httpd(addr))
  212. def _open_httpd(self, server_address):
  213. httpd = None
  214. try:
  215. # get address family for the server_address before
  216. # creating HttpServer object. If a specified address is
  217. # not numerical, gaierror may be thrown.
  218. address_family = socket.getaddrinfo(
  219. server_address[0], server_address[1], 0,
  220. socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_NUMERICHOST
  221. )[0][0]
  222. HttpServer.address_family = address_family
  223. httpd = HttpServer(
  224. server_address, HttpHandler,
  225. self.xml_handler, self.xsd_handler, self.xsl_handler,
  226. self.write_log)
  227. logger.info(STATHTTPD_STARTED, server_address[0],
  228. server_address[1])
  229. return httpd
  230. except (socket.gaierror, socket.error,
  231. OverflowError, TypeError) as err:
  232. if httpd:
  233. httpd.server_close()
  234. raise HttpServerError(
  235. "Invalid address %s, port %s: %s: %s" %
  236. (server_address[0], server_address[1],
  237. err.__class__.__name__, err))
  238. def close_httpd(self):
  239. """Closes sockets for HTTP"""
  240. while len(self.httpd)>0:
  241. ht = self.httpd.pop()
  242. logger.info(STATHTTPD_CLOSING, ht.server_address[0],
  243. ht.server_address[1])
  244. ht.server_close()
  245. def start(self):
  246. """Starts StatsHttpd objects to run. Waiting for client
  247. requests by using select.select functions"""
  248. self.running = True
  249. while self.running:
  250. try:
  251. (rfd, wfd, xfd) = select.select(
  252. self.get_sockets(), [], [], self.poll_intval)
  253. except select.error as err:
  254. # select.error exception is caught only in the case of
  255. # EINTR, or in other cases it is just thrown.
  256. if err.args[0] == errno.EINTR:
  257. (rfd, wfd, xfd) = ([], [], [])
  258. else:
  259. raise
  260. # FIXME: This module can handle only one request at a
  261. # time. If someone sends only part of the request, we block
  262. # waiting for it until we time out.
  263. # But it isn't so big issue for administration purposes.
  264. for fd in rfd + xfd:
  265. if fd == self.mccs.get_socket():
  266. self.mccs.check_command(nonblock=False)
  267. continue
  268. for ht in self.httpd:
  269. if fd == ht.socket:
  270. ht.handle_request()
  271. break
  272. self.stop()
  273. def stop(self):
  274. """Stops the running StatsHttpd objects. Closes CC session and
  275. HTTP handling sockets"""
  276. logger.info(STATHTTPD_SHUTDOWN)
  277. self.close_httpd()
  278. self.close_mccs()
  279. self.running = False
  280. def get_sockets(self):
  281. """Returns sockets to select.select"""
  282. sockets = []
  283. if self.mccs is not None:
  284. sockets.append(self.mccs.get_socket())
  285. if len(self.httpd) > 0:
  286. for ht in self.httpd:
  287. sockets.append(ht.socket)
  288. return sockets
  289. def config_handler(self, new_config):
  290. """Config handler for the ModuleCCSession object. It resets
  291. addresses and ports to listen HTTP requests on."""
  292. logger.debug(DBG_STATHTTPD_MESSAGING, STATHTTPD_HANDLE_CONFIG,
  293. new_config)
  294. errors = []
  295. if not self.mccs.get_module_spec().\
  296. validate_config(False, new_config, errors):
  297. return isc.config.ccsession.create_answer(
  298. 1, ", ".join(errors))
  299. # backup old config
  300. old_config = self.config.copy()
  301. self.load_config(new_config)
  302. # If the http sockets aren't opened or
  303. # if new_config doesn't have'listen_on', it returns
  304. if len(self.httpd) == 0 or 'listen_on' not in new_config:
  305. return isc.config.ccsession.create_answer(0)
  306. self.close_httpd()
  307. try:
  308. self.open_httpd()
  309. except HttpServerError as err:
  310. logger.error(STATHTTPD_SERVER_ERROR, err)
  311. # restore old config
  312. self.load_config(old_config)
  313. self.open_httpd()
  314. return isc.config.ccsession.create_answer(1, str(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. logger.debug(DBG_STATHTTPD_MESSAGING,
  322. STATHTTPD_RECEIVED_STATUS_COMMAND)
  323. return isc.config.ccsession.create_answer(
  324. 0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")")
  325. elif command == "shutdown":
  326. logger.debug(DBG_STATHTTPD_MESSAGING,
  327. STATHTTPD_RECEIVED_SHUTDOWN_COMMAND)
  328. self.running = False
  329. return isc.config.ccsession.create_answer(0)
  330. else:
  331. logger.debug(DBG_STATHTTPD_MESSAGING,
  332. STATHTTPD_RECEIVED_UNKNOWN_COMMAND, command)
  333. return isc.config.ccsession.create_answer(
  334. 1, "Unknown command: " + str(command))
  335. def get_stats_data(self, owner=None, name=None):
  336. """Requests statistics data to the Stats daemon and returns
  337. the data which obtains from it. The first argument is the
  338. module name which owns the statistics data, the second
  339. argument is one name of the statistics items which the the
  340. module owns. The second argument cannot be specified when the
  341. first argument is not specified. It returns the statistics
  342. data of the specified module or item. When the session timeout
  343. or the session error is occurred, it raises
  344. StatsHttpdError. When the stats daemon returns none-zero
  345. value, it raises StatsHttpdDataError."""
  346. param = {}
  347. if owner is None and name is None:
  348. param = None
  349. if owner is not None:
  350. param['owner'] = owner
  351. if name is not None:
  352. param['name'] = name
  353. try:
  354. seq = self.cc_session.group_sendmsg(
  355. isc.config.ccsession.create_command('show', param), 'Stats')
  356. (answer, env) = self.cc_session.group_recvmsg(False, seq)
  357. if answer:
  358. (rcode, value) = isc.config.ccsession.parse_answer(answer)
  359. except (isc.cc.session.SessionTimeout,
  360. isc.cc.session.SessionError) as err:
  361. raise StatsHttpdError("%s: %s" %
  362. (err.__class__.__name__, err))
  363. else:
  364. if rcode == 0:
  365. return value
  366. else:
  367. raise StatsHttpdDataError("Stats module: %s" % str(value))
  368. def get_stats_spec(self, owner=None, name=None):
  369. """Requests statistics data to the Stats daemon and returns
  370. the data which obtains from it. The first argument is the
  371. module name which owns the statistics data, the second
  372. argument is one name of the statistics items which the the
  373. module owns. The second argument cannot be specified when the
  374. first argument is not specified. It returns the statistics
  375. specification of the specified module or item. When the
  376. session timeout or the session error is occurred, it raises
  377. StatsHttpdError. When the stats daemon returns none-zero
  378. value, it raises StatsHttpdDataError."""
  379. param = {}
  380. if owner is None and name is None:
  381. param = None
  382. if owner is not None:
  383. param['owner'] = owner
  384. if name is not None:
  385. param['name'] = name
  386. try:
  387. seq = self.cc_session.group_sendmsg(
  388. isc.config.ccsession.create_command('showschema', param), 'Stats')
  389. (answer, env) = self.cc_session.group_recvmsg(False, seq)
  390. if answer:
  391. (rcode, value) = isc.config.ccsession.parse_answer(answer)
  392. if rcode == 0:
  393. return value
  394. else:
  395. raise StatsHttpdDataError("Stats module: %s" % str(value))
  396. except (isc.cc.session.SessionTimeout,
  397. isc.cc.session.SessionError) as err:
  398. raise StatsHttpdError("%s: %s" %
  399. (err.__class__.__name__, err))
  400. def xml_handler(self, module_name=None, item_name=None):
  401. """Requests the specified statistics data and specification by
  402. using the functions get_stats_data and get_stats_spec
  403. respectively and loads the XML template file and returns the
  404. string of the XML document.The first argument is the module
  405. name which owns the statistics data, the second argument is
  406. one name of the statistics items which the the module
  407. owns. The second argument cannot be specified when the first
  408. argument is not specified."""
  409. # TODO: Separate the following recursive function by type of
  410. # the parameter. Because we should be sure what type there is
  411. # when we call it recursively.
  412. def stats_data2xml(stats_spec, stats_data, xml_elem):
  413. """Internal use for xml_handler. Reads stats_data and
  414. stats_spec specified as first and second arguments, and
  415. modify the xml object specified as third
  416. argument. xml_elem must be modified and always returns
  417. None."""
  418. # assumed started with module_spec or started with
  419. # item_spec in statistics
  420. if type(stats_spec) is dict:
  421. # assumed started with module_spec
  422. if 'item_name' not in stats_spec \
  423. and 'item_type' not in stats_spec:
  424. for module_name in stats_spec.keys():
  425. elem = xml.etree.ElementTree.Element(module_name)
  426. stats_data2xml(stats_spec[module_name],
  427. stats_data[module_name], elem)
  428. xml_elem.append(elem)
  429. # started with item_spec in statistics
  430. else:
  431. elem = xml.etree.ElementTree.Element(stats_spec['item_name'])
  432. if stats_spec['item_type'] == 'map':
  433. stats_data2xml(stats_spec['map_item_spec'],
  434. stats_data,
  435. elem)
  436. elif stats_spec['item_type'] == 'list':
  437. for item in stats_data:
  438. stats_data2xml(stats_spec['list_item_spec'],
  439. item, elem)
  440. else:
  441. elem.text = str(stats_data)
  442. xml_elem.append(elem)
  443. # assumed started with stats_spec
  444. elif type(stats_spec) is list:
  445. for item_spec in stats_spec:
  446. stats_data2xml(item_spec,
  447. stats_data[item_spec['item_name']],
  448. xml_elem)
  449. stats_spec = self.get_stats_spec(module_name, item_name)
  450. stats_data = self.get_stats_data(module_name, item_name)
  451. # make the path xxx/module/item if specified respectively
  452. path_info = ''
  453. if module_name is not None and item_name is not None:
  454. path_info = '/' + module_name + '/' + item_name
  455. elif module_name is not None:
  456. path_info = '/' + module_name
  457. xml_elem = xml.etree.ElementTree.Element(
  458. 'bind10:statistics',
  459. attrib={ 'xsi:schemaLocation' : XSD_NAMESPACE + ' ' + XSD_URL_PATH + path_info,
  460. 'xmlns:bind10' : XSD_NAMESPACE,
  461. 'xmlns:xsi' : "http://www.w3.org/2001/XMLSchema-instance" })
  462. stats_data2xml(stats_spec, stats_data, xml_elem)
  463. # The coding conversion is tricky. xml..tostring() of Python 3.2
  464. # returns bytes (not string) regardless of the coding, while
  465. # tostring() of Python 3.1 returns a string. To support both
  466. # cases transparently, we first make sure tostring() returns
  467. # bytes by specifying utf-8 and then convert the result to a
  468. # plain string (code below assume it).
  469. # FIXME: Non-ASCII characters might be lost here. Consider how
  470. # the whole system should handle non-ASCII characters.
  471. xml_string = str(xml.etree.ElementTree.tostring(xml_elem, encoding='utf-8'),
  472. encoding='us-ascii')
  473. self.xml_body = self.open_template(XML_TEMPLATE_LOCATION).substitute(
  474. xml_string=xml_string,
  475. xsl_url_path=XSL_URL_PATH + path_info)
  476. assert self.xml_body is not None
  477. return self.xml_body
  478. def xsd_handler(self, module_name=None, item_name=None):
  479. """Requests the specified statistics specification by using
  480. the function get_stats_spec respectively and loads the XSD
  481. template file and returns the string of the XSD document.The
  482. first argument is the module name which owns the statistics
  483. data, the second argument is one name of the statistics items
  484. which the the module owns. The second argument cannot be
  485. specified when the first argument is not specified."""
  486. # TODO: Separate the following recursive function by type of
  487. # the parameter. Because we should be sure what type there is
  488. # when we call it recursively.
  489. def stats_spec2xsd(stats_spec, xsd_elem):
  490. """Internal use for xsd_handler. Reads stats_spec
  491. specified as first arguments, and modify the xml object
  492. specified as second argument. xsd_elem must be
  493. modified. Always returns None with no exceptions."""
  494. # assumed module_spec or one stats_spec
  495. if type(stats_spec) is dict:
  496. # assumed module_spec
  497. if 'item_name' not in stats_spec:
  498. for mod in stats_spec.keys():
  499. elem = xml.etree.ElementTree.Element(
  500. "element", { "name" : mod })
  501. complextype = xml.etree.ElementTree.Element("complexType")
  502. alltag = xml.etree.ElementTree.Element("all")
  503. stats_spec2xsd(stats_spec[mod], alltag)
  504. complextype.append(alltag)
  505. elem.append(complextype)
  506. xsd_elem.append(elem)
  507. # assumed stats_spec
  508. else:
  509. if stats_spec['item_type'] == 'map':
  510. alltag = xml.etree.ElementTree.Element("all")
  511. stats_spec2xsd(stats_spec['map_item_spec'], alltag)
  512. complextype = xml.etree.ElementTree.Element("complexType")
  513. complextype.append(alltag)
  514. elem = xml.etree.ElementTree.Element(
  515. "element", attrib={ "name" : stats_spec["item_name"],
  516. "minOccurs": "0" \
  517. if stats_spec["item_optional"] \
  518. else "1",
  519. "maxOccurs": "unbounded" })
  520. elem.append(complextype)
  521. xsd_elem.append(elem)
  522. elif stats_spec['item_type'] == 'list':
  523. alltag = xml.etree.ElementTree.Element("sequence")
  524. stats_spec2xsd(stats_spec['list_item_spec'], alltag)
  525. complextype = xml.etree.ElementTree.Element("complexType")
  526. complextype.append(alltag)
  527. elem = xml.etree.ElementTree.Element(
  528. "element", attrib={ "name" : stats_spec["item_name"],
  529. "minOccurs": "0" \
  530. if stats_spec["item_optional"] \
  531. else "1",
  532. "maxOccurs": "1" })
  533. elem.append(complextype)
  534. xsd_elem.append(elem)
  535. else:
  536. # determine the datatype of XSD
  537. # TODO: Should consider other item_format types
  538. datatype = stats_spec["item_type"] \
  539. if stats_spec["item_type"].lower() != 'real' \
  540. else 'float'
  541. if "item_format" in stats_spec:
  542. item_format = stats_spec["item_format"]
  543. if datatype.lower() == 'string' \
  544. and item_format.lower() == 'date-time':
  545. datatype = 'dateTime'
  546. elif datatype.lower() == 'string' \
  547. and (item_format.lower() == 'date' \
  548. or item_format.lower() == 'time'):
  549. datatype = item_format.lower()
  550. elem = xml.etree.ElementTree.Element(
  551. "element",
  552. attrib={
  553. 'name' : stats_spec["item_name"],
  554. 'type' : datatype,
  555. 'minOccurs' : "0" \
  556. if stats_spec["item_optional"] \
  557. else "1",
  558. 'maxOccurs' : "1"
  559. }
  560. )
  561. annotation = xml.etree.ElementTree.Element("annotation")
  562. appinfo = xml.etree.ElementTree.Element("appinfo")
  563. documentation = xml.etree.ElementTree.Element("documentation")
  564. if "item_title" in stats_spec:
  565. appinfo.text = stats_spec["item_title"]
  566. if "item_description" in stats_spec:
  567. documentation.text = stats_spec["item_description"]
  568. annotation.append(appinfo)
  569. annotation.append(documentation)
  570. elem.append(annotation)
  571. xsd_elem.append(elem)
  572. # multiple stats_specs
  573. elif type(stats_spec) is list:
  574. for item_spec in stats_spec:
  575. stats_spec2xsd(item_spec, xsd_elem)
  576. # for XSD
  577. stats_spec = self.get_stats_spec(module_name, item_name)
  578. alltag = xml.etree.ElementTree.Element("all")
  579. stats_spec2xsd(stats_spec, alltag)
  580. complextype = xml.etree.ElementTree.Element("complexType")
  581. complextype.append(alltag)
  582. documentation = xml.etree.ElementTree.Element("documentation")
  583. documentation.text = "A set of statistics data"
  584. annotation = xml.etree.ElementTree.Element("annotation")
  585. annotation.append(documentation)
  586. elem = xml.etree.ElementTree.Element(
  587. "element", attrib={ 'name' : 'statistics' })
  588. elem.append(annotation)
  589. elem.append(complextype)
  590. documentation = xml.etree.ElementTree.Element("documentation")
  591. documentation.text = "XML schema of the statistics data in BIND 10"
  592. annotation = xml.etree.ElementTree.Element("annotation")
  593. annotation.append(documentation)
  594. xsd_root = xml.etree.ElementTree.Element(
  595. "schema",
  596. attrib={ 'xmlns' : "http://www.w3.org/2001/XMLSchema",
  597. 'targetNamespace' : XSD_NAMESPACE,
  598. 'xmlns:bind10' : XSD_NAMESPACE })
  599. xsd_root.append(annotation)
  600. xsd_root.append(elem)
  601. # The coding conversion is tricky. xml..tostring() of Python 3.2
  602. # returns bytes (not string) regardless of the coding, while
  603. # tostring() of Python 3.1 returns a string. To support both
  604. # cases transparently, we first make sure tostring() returns
  605. # bytes by specifying utf-8 and then convert the result to a
  606. # plain string (code below assume it).
  607. # FIXME: Non-ASCII characters might be lost here. Consider how
  608. # the whole system should handle non-ASCII characters.
  609. xsd_string = str(xml.etree.ElementTree.tostring(xsd_root, encoding='utf-8'),
  610. encoding='us-ascii')
  611. self.xsd_body = self.open_template(XSD_TEMPLATE_LOCATION).substitute(
  612. xsd_string=xsd_string)
  613. assert self.xsd_body is not None
  614. return self.xsd_body
  615. def xsl_handler(self, module_name=None, item_name=None):
  616. """Requests the specified statistics specification by using
  617. the function get_stats_spec respectively and loads the XSL
  618. template file and returns the string of the XSL document.The
  619. first argument is the module name which owns the statistics
  620. data, the second argument is one name of the statistics items
  621. which the the module owns. The second argument cannot be
  622. specified when the first argument is not specified."""
  623. # TODO: Separate the following recursive function by type of
  624. # the parameter. Because we should be sure what type there is
  625. # when we call it recursively.
  626. def stats_spec2xsl(stats_spec, xsl_elem, path=XML_URL_PATH):
  627. """Internal use for xsl_handler. Reads stats_spec
  628. specified as first arguments, and modify the xml object
  629. specified as second argument. xsl_elem must be
  630. modified. The third argument is a base path used for
  631. making anchor tag in XSL. Always returns None with no
  632. exceptions."""
  633. # assumed module_spec or one stats_spec
  634. if type(stats_spec) is dict:
  635. # assumed module_spec
  636. if 'item_name' not in stats_spec:
  637. table = xml.etree.ElementTree.Element("table")
  638. tr = xml.etree.ElementTree.Element("tr")
  639. th = xml.etree.ElementTree.Element("th")
  640. th.text = "Module Name"
  641. tr.append(th)
  642. th = xml.etree.ElementTree.Element("th")
  643. th.text = "Module Item"
  644. tr.append(th)
  645. table.append(tr)
  646. for mod in stats_spec.keys():
  647. foreach = xml.etree.ElementTree.Element(
  648. "xsl:for-each", attrib={ "select" : mod })
  649. tr = xml.etree.ElementTree.Element("tr")
  650. td = xml.etree.ElementTree.Element("td")
  651. a = xml.etree.ElementTree.Element(
  652. "a", attrib={ "href": urllib.parse.quote(path + "/" + mod) })
  653. a.text = mod
  654. td.append(a)
  655. tr.append(td)
  656. td = xml.etree.ElementTree.Element("td")
  657. stats_spec2xsl(stats_spec[mod], td,
  658. path + "/" + mod)
  659. tr.append(td)
  660. foreach.append(tr)
  661. table.append(foreach)
  662. xsl_elem.append(table)
  663. # assumed stats_spec
  664. else:
  665. if stats_spec['item_type'] == 'map':
  666. table = xml.etree.ElementTree.Element("table")
  667. tr = xml.etree.ElementTree.Element("tr")
  668. th = xml.etree.ElementTree.Element("th")
  669. th.text = "Item Name"
  670. tr.append(th)
  671. th = xml.etree.ElementTree.Element("th")
  672. th.text = "Item Value"
  673. tr.append(th)
  674. table.append(tr)
  675. foreach = xml.etree.ElementTree.Element(
  676. "xsl:for-each", attrib={ "select" : stats_spec['item_name'] })
  677. tr = xml.etree.ElementTree.Element("tr")
  678. td = xml.etree.ElementTree.Element(
  679. "td",
  680. attrib={ "class" : "title",
  681. "title" : stats_spec["item_description"] \
  682. if "item_description" in stats_spec \
  683. else "" })
  684. # TODO: Consider whether we should always use
  685. # the identical name "item_name" for the
  686. # user-visible name in XSL.
  687. td.text = stats_spec[ "item_title" if "item_title" in stats_spec else "item_name" ]
  688. tr.append(td)
  689. td = xml.etree.ElementTree.Element("td")
  690. stats_spec2xsl(stats_spec['map_item_spec'], td,
  691. path + "/" + stats_spec["item_name"])
  692. tr.append(td)
  693. foreach.append(tr)
  694. table.append(foreach)
  695. xsl_elem.append(table)
  696. elif stats_spec['item_type'] == 'list':
  697. stats_spec2xsl(stats_spec['list_item_spec'], xsl_elem,
  698. path + "/" + stats_spec["item_name"])
  699. else:
  700. xsl_valueof = xml.etree.ElementTree.Element(
  701. "xsl:value-of",
  702. attrib={'select': stats_spec["item_name"]})
  703. xsl_elem.append(xsl_valueof)
  704. # multiple stats_specs
  705. elif type(stats_spec) is list:
  706. table = xml.etree.ElementTree.Element("table")
  707. tr = xml.etree.ElementTree.Element("tr")
  708. th = xml.etree.ElementTree.Element("th")
  709. th.text = "Item Name"
  710. tr.append(th)
  711. th = xml.etree.ElementTree.Element("th")
  712. th.text = "Item Value"
  713. tr.append(th)
  714. table.append(tr)
  715. for item_spec in stats_spec:
  716. tr = xml.etree.ElementTree.Element("tr")
  717. td = xml.etree.ElementTree.Element(
  718. "td",
  719. attrib={ "class" : "title",
  720. "title" : item_spec["item_description"] \
  721. if "item_description" in item_spec \
  722. else "" })
  723. # if the path length is equal to or shorter than
  724. # XML_URL_PATH + /Module/Item, add the anchor tag.
  725. if len(path.split('/')) <= len((XML_URL_PATH + '/Module/Item').split('/')):
  726. a = xml.etree.ElementTree.Element(
  727. "a", attrib={ "href": urllib.parse.quote(path + "/" + item_spec["item_name"]) })
  728. a.text = item_spec[ "item_title" if "item_title" in item_spec else "item_name" ]
  729. td.append(a)
  730. else:
  731. td.text = item_spec[ "item_title" if "item_title" in item_spec else "item_name" ]
  732. tr.append(td)
  733. td = xml.etree.ElementTree.Element("td")
  734. stats_spec2xsl(item_spec, td, path)
  735. tr.append(td)
  736. if item_spec['item_type'] == 'list':
  737. foreach = xml.etree.ElementTree.Element(
  738. "xsl:for-each", attrib={ "select" : item_spec['item_name'] })
  739. foreach.append(tr)
  740. table.append(foreach)
  741. else:
  742. table.append(tr)
  743. xsl_elem.append(table)
  744. # for XSL
  745. stats_spec = self.get_stats_spec(module_name, item_name)
  746. xsd_root = xml.etree.ElementTree.Element( # started with xml:template tag
  747. "xsl:template",
  748. attrib={'match': "bind10:statistics"})
  749. stats_spec2xsl(stats_spec, xsd_root)
  750. # The coding conversion is tricky. xml..tostring() of Python 3.2
  751. # returns bytes (not string) regardless of the coding, while
  752. # tostring() of Python 3.1 returns a string. To support both
  753. # cases transparently, we first make sure tostring() returns
  754. # bytes by specifying utf-8 and then convert the result to a
  755. # plain string (code below assume it).
  756. # FIXME: Non-ASCII characters might be lost here. Consider how
  757. # the whole system should handle non-ASCII characters.
  758. xsl_string = str(xml.etree.ElementTree.tostring(xsd_root, encoding='utf-8'),
  759. encoding='us-ascii')
  760. self.xsl_body = self.open_template(XSL_TEMPLATE_LOCATION).substitute(
  761. xsl_string=xsl_string,
  762. xsd_namespace=XSD_NAMESPACE)
  763. assert self.xsl_body is not None
  764. return self.xsl_body
  765. def open_template(self, file_name):
  766. """It opens a template file, and it loads all lines to a
  767. string variable and returns string. Template object includes
  768. the variable. Limitation of a file size isn't needed there."""
  769. f = open(file_name, 'r')
  770. lines = "".join(f.readlines())
  771. f.close()
  772. assert lines is not None
  773. return string.Template(lines)
  774. if __name__ == "__main__":
  775. try:
  776. parser = OptionParser()
  777. parser.add_option(
  778. "-v", "--verbose", dest="verbose", action="store_true",
  779. help="display more about what is going on")
  780. (options, args) = parser.parse_args()
  781. if options.verbose:
  782. isc.log.init("b10-stats-httpd", "DEBUG", 99)
  783. stats_httpd = StatsHttpd()
  784. stats_httpd.start()
  785. except OptionValueError as ove:
  786. logger.fatal(STATHTTPD_BAD_OPTION_VALUE, ove)
  787. sys.exit(1)
  788. except isc.cc.session.SessionError as se:
  789. logger.fatal(STATHTTPD_CC_SESSION_ERROR, se)
  790. sys.exit(1)
  791. except HttpServerError as hse:
  792. logger.fatal(STATHTTPD_START_SERVER_INIT_ERROR, hse)
  793. sys.exit(1)
  794. except KeyboardInterrupt as kie:
  795. logger.info(STATHTTPD_STOPPED_BY_KEYBOARD)