stats_httpd.py.in 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  1. #!@PYTHON@
  2. # Copyright (C) 2011-2012 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. try:
  175. self.open_httpd()
  176. except HttpServerError:
  177. # if some exception, e.g. address in use, is raised, then it closes mccs and httpd
  178. self.close_mccs()
  179. raise
  180. def open_mccs(self):
  181. """Opens a ModuleCCSession object"""
  182. # create ModuleCCSession
  183. logger.debug(DBG_STATHTTPD_INIT, STATHTTPD_STARTING_CC_SESSION)
  184. self.mccs = isc.config.ModuleCCSession(
  185. SPECFILE_LOCATION, self.config_handler, self.command_handler)
  186. self.cc_session = self.mccs._session
  187. def close_mccs(self):
  188. """Closes a ModuleCCSession object"""
  189. if self.mccs is None:
  190. return
  191. self.mccs.send_stopping()
  192. logger.debug(DBG_STATHTTPD_INIT, STATHTTPD_CLOSING_CC_SESSION)
  193. self.mccs.close()
  194. self.mccs = None
  195. def load_config(self, new_config={}):
  196. """Loads configuration from spec file or new configuration
  197. from the config manager"""
  198. # load config
  199. if len(self.config) == 0:
  200. self.config = dict([
  201. (itm['item_name'], self.mccs.get_value(itm['item_name'])[0])
  202. for itm in self.mccs.get_module_spec().get_config_spec()
  203. ])
  204. self.config.update(new_config)
  205. # set addresses and ports for HTTP
  206. addrs = []
  207. if 'listen_on' in self.config:
  208. for cf in self.config['listen_on']:
  209. if 'address' in cf and 'port' in cf:
  210. addrs.append((cf['address'], cf['port']))
  211. self.http_addrs = addrs
  212. def open_httpd(self):
  213. """Opens sockets for HTTP. Iterating each HTTP address to be
  214. configured in spec file"""
  215. for addr in self.http_addrs:
  216. self.httpd.append(self._open_httpd(addr))
  217. def _open_httpd(self, server_address):
  218. httpd = None
  219. try:
  220. # get address family for the server_address before
  221. # creating HttpServer object. If a specified address is
  222. # not numerical, gaierror may be thrown.
  223. address_family = socket.getaddrinfo(
  224. server_address[0], server_address[1], 0,
  225. socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_NUMERICHOST
  226. )[0][0]
  227. HttpServer.address_family = address_family
  228. httpd = HttpServer(
  229. server_address, HttpHandler,
  230. self.xml_handler, self.xsd_handler, self.xsl_handler,
  231. self.write_log)
  232. logger.info(STATHTTPD_STARTED, server_address[0],
  233. server_address[1])
  234. return httpd
  235. except (socket.gaierror, socket.error,
  236. OverflowError, TypeError) as err:
  237. if httpd:
  238. httpd.server_close()
  239. raise HttpServerError(
  240. "Invalid address %s, port %s: %s: %s" %
  241. (server_address[0], server_address[1],
  242. err.__class__.__name__, err))
  243. def close_httpd(self):
  244. """Closes sockets for HTTP"""
  245. while len(self.httpd)>0:
  246. ht = self.httpd.pop()
  247. logger.info(STATHTTPD_CLOSING, ht.server_address[0],
  248. ht.server_address[1])
  249. ht.server_close()
  250. def start(self):
  251. """Starts StatsHttpd objects to run. Waiting for client
  252. requests by using select.select functions"""
  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. # select.error exception is caught only in the case of
  260. # EINTR, or in other cases it is just thrown.
  261. if err.args[0] == errno.EINTR:
  262. (rfd, wfd, xfd) = ([], [], [])
  263. else:
  264. raise
  265. # FIXME: This module can handle only one request at a
  266. # time. If someone sends only part of the request, we block
  267. # waiting for it until we time out.
  268. # But it isn't so big issue for administration purposes.
  269. for fd in rfd + xfd:
  270. if fd == self.mccs.get_socket():
  271. self.mccs.check_command(nonblock=False)
  272. continue
  273. for ht in self.httpd:
  274. if fd == ht.socket:
  275. ht.handle_request()
  276. break
  277. self.stop()
  278. def stop(self):
  279. """Stops the running StatsHttpd objects. Closes CC session and
  280. HTTP handling sockets"""
  281. logger.info(STATHTTPD_SHUTDOWN)
  282. self.close_httpd()
  283. self.close_mccs()
  284. self.running = False
  285. def get_sockets(self):
  286. """Returns sockets to select.select"""
  287. sockets = []
  288. if self.mccs is not None:
  289. sockets.append(self.mccs.get_socket())
  290. if len(self.httpd) > 0:
  291. for ht in self.httpd:
  292. sockets.append(ht.socket)
  293. return sockets
  294. def config_handler(self, new_config):
  295. """Config handler for the ModuleCCSession object. It resets
  296. addresses and ports to listen HTTP requests on."""
  297. logger.debug(DBG_STATHTTPD_MESSAGING, STATHTTPD_HANDLE_CONFIG,
  298. new_config)
  299. errors = []
  300. if not self.mccs.get_module_spec().\
  301. validate_config(False, new_config, errors):
  302. return isc.config.ccsession.create_answer(
  303. 1, ", ".join(errors))
  304. # backup old config
  305. old_config = self.config.copy()
  306. self.load_config(new_config)
  307. # If the http sockets aren't opened or
  308. # if new_config doesn't have'listen_on', it returns
  309. if len(self.httpd) == 0 or 'listen_on' not in new_config:
  310. return isc.config.ccsession.create_answer(0)
  311. self.close_httpd()
  312. try:
  313. self.open_httpd()
  314. except HttpServerError as err:
  315. logger.error(STATHTTPD_SERVER_ERROR, err)
  316. # restore old config
  317. self.load_config(old_config)
  318. self.open_httpd()
  319. return isc.config.ccsession.create_answer(1, str(err))
  320. else:
  321. return isc.config.ccsession.create_answer(0)
  322. def command_handler(self, command, args):
  323. """Command handler for the ModuleCCSesson object. It handles
  324. "status" and "shutdown" commands."""
  325. if command == "status":
  326. logger.debug(DBG_STATHTTPD_MESSAGING,
  327. STATHTTPD_RECEIVED_STATUS_COMMAND)
  328. return isc.config.ccsession.create_answer(
  329. 0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")")
  330. elif command == "shutdown":
  331. logger.debug(DBG_STATHTTPD_MESSAGING,
  332. STATHTTPD_RECEIVED_SHUTDOWN_COMMAND)
  333. self.running = False
  334. return isc.config.ccsession.create_answer(0)
  335. else:
  336. logger.debug(DBG_STATHTTPD_MESSAGING,
  337. STATHTTPD_RECEIVED_UNKNOWN_COMMAND, command)
  338. return isc.config.ccsession.create_answer(
  339. 1, "Unknown command: " + str(command))
  340. def get_stats_data(self, owner=None, name=None):
  341. """Requests statistics data to the Stats daemon and returns
  342. the data which obtains from it. The first argument is the
  343. module name which owns the statistics data, the second
  344. argument is one name of the statistics items which the the
  345. module owns. The second argument cannot be specified when the
  346. first argument is not specified. It returns the statistics
  347. data of the specified module or item. When the session timeout
  348. or the session error is occurred, it raises
  349. StatsHttpdError. When the stats daemon returns none-zero
  350. value, it raises StatsHttpdDataError."""
  351. param = {}
  352. if owner is None and name is None:
  353. param = None
  354. if owner is not None:
  355. param['owner'] = owner
  356. if name is not None:
  357. param['name'] = name
  358. try:
  359. seq = self.cc_session.group_sendmsg(
  360. isc.config.ccsession.create_command('show', param), 'Stats')
  361. (answer, env) = self.cc_session.group_recvmsg(False, seq)
  362. if answer:
  363. (rcode, value) = isc.config.ccsession.parse_answer(answer)
  364. except (isc.cc.session.SessionTimeout,
  365. isc.cc.session.SessionError) as err:
  366. raise StatsHttpdError("%s: %s" %
  367. (err.__class__.__name__, err))
  368. else:
  369. if rcode == 0:
  370. return value
  371. else:
  372. raise StatsHttpdDataError("Stats module: %s" % str(value))
  373. def get_stats_spec(self, owner=None, name=None):
  374. """Requests statistics data to the Stats daemon and returns
  375. the data which obtains from it. The first argument is the
  376. module name which owns the statistics data, the second
  377. argument is one name of the statistics items which the the
  378. module owns. The second argument cannot be specified when the
  379. first argument is not specified. It returns the statistics
  380. specification of the specified module or item. When the
  381. session timeout or the session error is occurred, it raises
  382. StatsHttpdError. When the stats daemon returns none-zero
  383. value, it raises StatsHttpdDataError."""
  384. param = {}
  385. if owner is None and name is None:
  386. param = None
  387. if owner is not None:
  388. param['owner'] = owner
  389. if name is not None:
  390. param['name'] = name
  391. try:
  392. seq = self.cc_session.group_sendmsg(
  393. isc.config.ccsession.create_command('showschema', param), 'Stats')
  394. (answer, env) = self.cc_session.group_recvmsg(False, seq)
  395. if answer:
  396. (rcode, value) = isc.config.ccsession.parse_answer(answer)
  397. if rcode == 0:
  398. return value
  399. else:
  400. raise StatsHttpdDataError("Stats module: %s" % str(value))
  401. except (isc.cc.session.SessionTimeout,
  402. isc.cc.session.SessionError) as err:
  403. raise StatsHttpdError("%s: %s" %
  404. (err.__class__.__name__, err))
  405. def xml_handler(self, module_name=None, item_name=None):
  406. """Requests the specified statistics data and specification by
  407. using the functions get_stats_data and get_stats_spec
  408. respectively and loads the XML template file and returns the
  409. string of the XML document.The first argument is the module
  410. name which owns the statistics data, the second argument is
  411. one name of the statistics items which the the module
  412. owns. The second argument cannot be specified when the first
  413. argument is not specified."""
  414. # TODO: Separate the following recursive function by type of
  415. # the parameter. Because we should be sure what type there is
  416. # when we call it recursively.
  417. def stats_data2xml(stats_spec, stats_data, xml_elem):
  418. """Internal use for xml_handler. Reads stats_data and
  419. stats_spec specified as first and second arguments, and
  420. modify the xml object specified as third
  421. argument. xml_elem must be modified and always returns
  422. None."""
  423. # assumed started with module_spec or started with
  424. # item_spec in statistics
  425. if type(stats_spec) is dict:
  426. # assumed started with module_spec
  427. if 'item_name' not in stats_spec \
  428. and 'item_type' not in stats_spec:
  429. for module_name in stats_spec.keys():
  430. elem = xml.etree.ElementTree.Element(module_name)
  431. stats_data2xml(stats_spec[module_name],
  432. stats_data[module_name], elem)
  433. xml_elem.append(elem)
  434. # started with item_spec in statistics
  435. else:
  436. elem = xml.etree.ElementTree.Element(stats_spec['item_name'])
  437. if stats_spec['item_type'] == 'map':
  438. stats_data2xml(stats_spec['map_item_spec'],
  439. stats_data,
  440. elem)
  441. elif stats_spec['item_type'] == 'list':
  442. for item in stats_data:
  443. stats_data2xml(stats_spec['list_item_spec'],
  444. item, elem)
  445. else:
  446. elem.text = str(stats_data)
  447. xml_elem.append(elem)
  448. # assumed started with stats_spec
  449. elif type(stats_spec) is list:
  450. for item_spec in stats_spec:
  451. stats_data2xml(item_spec,
  452. stats_data[item_spec['item_name']],
  453. xml_elem)
  454. stats_spec = self.get_stats_spec(module_name, item_name)
  455. stats_data = self.get_stats_data(module_name, item_name)
  456. # make the path xxx/module/item if specified respectively
  457. path_info = ''
  458. if module_name is not None and item_name is not None:
  459. path_info = '/' + module_name + '/' + item_name
  460. elif module_name is not None:
  461. path_info = '/' + module_name
  462. xml_elem = xml.etree.ElementTree.Element(
  463. 'bind10:statistics',
  464. attrib={ 'xsi:schemaLocation' : XSD_NAMESPACE + ' ' + XSD_URL_PATH + path_info,
  465. 'xmlns:bind10' : XSD_NAMESPACE,
  466. 'xmlns:xsi' : "http://www.w3.org/2001/XMLSchema-instance" })
  467. stats_data2xml(stats_spec, stats_data, xml_elem)
  468. # The coding conversion is tricky. xml..tostring() of Python 3.2
  469. # returns bytes (not string) regardless of the coding, while
  470. # tostring() of Python 3.1 returns a string. To support both
  471. # cases transparently, we first make sure tostring() returns
  472. # bytes by specifying utf-8 and then convert the result to a
  473. # plain string (code below assume it).
  474. # FIXME: Non-ASCII characters might be lost here. Consider how
  475. # the whole system should handle non-ASCII characters.
  476. xml_string = str(xml.etree.ElementTree.tostring(xml_elem, encoding='utf-8'),
  477. encoding='us-ascii')
  478. self.xml_body = self.open_template(XML_TEMPLATE_LOCATION).substitute(
  479. xml_string=xml_string,
  480. xsl_url_path=XSL_URL_PATH + path_info)
  481. assert self.xml_body is not None
  482. return self.xml_body
  483. def xsd_handler(self, module_name=None, item_name=None):
  484. """Requests the specified statistics specification by using
  485. the function get_stats_spec respectively and loads the XSD
  486. template file and returns the string of the XSD document.The
  487. first argument is the module name which owns the statistics
  488. data, the second argument is one name of the statistics items
  489. which the the module owns. The second argument cannot be
  490. specified when the first argument is not specified."""
  491. # TODO: Separate the following recursive function by type of
  492. # the parameter. Because we should be sure what type there is
  493. # when we call it recursively.
  494. def stats_spec2xsd(stats_spec, xsd_elem):
  495. """Internal use for xsd_handler. Reads stats_spec
  496. specified as first arguments, and modify the xml object
  497. specified as second argument. xsd_elem must be
  498. modified. Always returns None with no exceptions."""
  499. # assumed module_spec or one stats_spec
  500. if type(stats_spec) is dict:
  501. # assumed module_spec
  502. if 'item_name' not in stats_spec:
  503. for mod in stats_spec.keys():
  504. elem = xml.etree.ElementTree.Element(
  505. "element", { "name" : mod })
  506. complextype = xml.etree.ElementTree.Element("complexType")
  507. alltag = xml.etree.ElementTree.Element("all")
  508. stats_spec2xsd(stats_spec[mod], alltag)
  509. complextype.append(alltag)
  510. elem.append(complextype)
  511. xsd_elem.append(elem)
  512. # assumed stats_spec
  513. else:
  514. if stats_spec['item_type'] == 'map':
  515. alltag = xml.etree.ElementTree.Element("all")
  516. stats_spec2xsd(stats_spec['map_item_spec'], alltag)
  517. complextype = xml.etree.ElementTree.Element("complexType")
  518. complextype.append(alltag)
  519. elem = xml.etree.ElementTree.Element(
  520. "element", attrib={ "name" : stats_spec["item_name"],
  521. "minOccurs": "0" \
  522. if stats_spec["item_optional"] \
  523. else "1",
  524. "maxOccurs": "unbounded" })
  525. elem.append(complextype)
  526. xsd_elem.append(elem)
  527. elif stats_spec['item_type'] == 'list':
  528. alltag = xml.etree.ElementTree.Element("sequence")
  529. stats_spec2xsd(stats_spec['list_item_spec'], alltag)
  530. complextype = xml.etree.ElementTree.Element("complexType")
  531. complextype.append(alltag)
  532. elem = xml.etree.ElementTree.Element(
  533. "element", attrib={ "name" : stats_spec["item_name"],
  534. "minOccurs": "0" \
  535. if stats_spec["item_optional"] \
  536. else "1",
  537. "maxOccurs": "1" })
  538. elem.append(complextype)
  539. xsd_elem.append(elem)
  540. else:
  541. # determine the datatype of XSD
  542. # TODO: Should consider other item_format types
  543. datatype = stats_spec["item_type"] \
  544. if stats_spec["item_type"].lower() != 'real' \
  545. else 'float'
  546. if "item_format" in stats_spec:
  547. item_format = stats_spec["item_format"]
  548. if datatype.lower() == 'string' \
  549. and item_format.lower() == 'date-time':
  550. datatype = 'dateTime'
  551. elif datatype.lower() == 'string' \
  552. and (item_format.lower() == 'date' \
  553. or item_format.lower() == 'time'):
  554. datatype = item_format.lower()
  555. elem = xml.etree.ElementTree.Element(
  556. "element",
  557. attrib={
  558. 'name' : stats_spec["item_name"],
  559. 'type' : datatype,
  560. 'minOccurs' : "0" \
  561. if stats_spec["item_optional"] \
  562. else "1",
  563. 'maxOccurs' : "1"
  564. }
  565. )
  566. annotation = xml.etree.ElementTree.Element("annotation")
  567. appinfo = xml.etree.ElementTree.Element("appinfo")
  568. documentation = xml.etree.ElementTree.Element("documentation")
  569. if "item_title" in stats_spec:
  570. appinfo.text = stats_spec["item_title"]
  571. if "item_description" in stats_spec:
  572. documentation.text = stats_spec["item_description"]
  573. annotation.append(appinfo)
  574. annotation.append(documentation)
  575. elem.append(annotation)
  576. xsd_elem.append(elem)
  577. # multiple stats_specs
  578. elif type(stats_spec) is list:
  579. for item_spec in stats_spec:
  580. stats_spec2xsd(item_spec, xsd_elem)
  581. # for XSD
  582. stats_spec = self.get_stats_spec(module_name, item_name)
  583. alltag = xml.etree.ElementTree.Element("all")
  584. stats_spec2xsd(stats_spec, alltag)
  585. complextype = xml.etree.ElementTree.Element("complexType")
  586. complextype.append(alltag)
  587. documentation = xml.etree.ElementTree.Element("documentation")
  588. documentation.text = "A set of statistics data"
  589. annotation = xml.etree.ElementTree.Element("annotation")
  590. annotation.append(documentation)
  591. elem = xml.etree.ElementTree.Element(
  592. "element", attrib={ 'name' : 'statistics' })
  593. elem.append(annotation)
  594. elem.append(complextype)
  595. documentation = xml.etree.ElementTree.Element("documentation")
  596. documentation.text = "XML schema of the statistics data in BIND 10"
  597. annotation = xml.etree.ElementTree.Element("annotation")
  598. annotation.append(documentation)
  599. xsd_root = xml.etree.ElementTree.Element(
  600. "schema",
  601. attrib={ 'xmlns' : "http://www.w3.org/2001/XMLSchema",
  602. 'targetNamespace' : XSD_NAMESPACE,
  603. 'xmlns:bind10' : XSD_NAMESPACE })
  604. xsd_root.append(annotation)
  605. xsd_root.append(elem)
  606. # The coding conversion is tricky. xml..tostring() of Python 3.2
  607. # returns bytes (not string) regardless of the coding, while
  608. # tostring() of Python 3.1 returns a string. To support both
  609. # cases transparently, we first make sure tostring() returns
  610. # bytes by specifying utf-8 and then convert the result to a
  611. # plain string (code below assume it).
  612. # FIXME: Non-ASCII characters might be lost here. Consider how
  613. # the whole system should handle non-ASCII characters.
  614. xsd_string = str(xml.etree.ElementTree.tostring(xsd_root, encoding='utf-8'),
  615. encoding='us-ascii')
  616. self.xsd_body = self.open_template(XSD_TEMPLATE_LOCATION).substitute(
  617. xsd_string=xsd_string)
  618. assert self.xsd_body is not None
  619. return self.xsd_body
  620. def xsl_handler(self, module_name=None, item_name=None):
  621. """Requests the specified statistics specification by using
  622. the function get_stats_spec respectively and loads the XSL
  623. template file and returns the string of the XSL document.The
  624. first argument is the module name which owns the statistics
  625. data, the second argument is one name of the statistics items
  626. which the the module owns. The second argument cannot be
  627. specified when the first argument is not specified."""
  628. # TODO: Separate the following recursive function by type of
  629. # the parameter. Because we should be sure what type there is
  630. # when we call it recursively.
  631. def stats_spec2xsl(stats_spec, xsl_elem, path=XML_URL_PATH):
  632. """Internal use for xsl_handler. Reads stats_spec
  633. specified as first arguments, and modify the xml object
  634. specified as second argument. xsl_elem must be
  635. modified. The third argument is a base path used for
  636. making anchor tag in XSL. Always returns None with no
  637. exceptions."""
  638. # assumed module_spec or one stats_spec
  639. if type(stats_spec) is dict:
  640. # assumed module_spec
  641. if 'item_name' not in stats_spec:
  642. table = xml.etree.ElementTree.Element("table")
  643. tr = xml.etree.ElementTree.Element("tr")
  644. th = xml.etree.ElementTree.Element("th")
  645. th.text = "Module Name"
  646. tr.append(th)
  647. th = xml.etree.ElementTree.Element("th")
  648. th.text = "Module Item"
  649. tr.append(th)
  650. table.append(tr)
  651. for mod in stats_spec.keys():
  652. foreach = xml.etree.ElementTree.Element(
  653. "xsl:for-each", attrib={ "select" : mod })
  654. tr = xml.etree.ElementTree.Element("tr")
  655. td = xml.etree.ElementTree.Element("td")
  656. a = xml.etree.ElementTree.Element(
  657. "a", attrib={ "href": urllib.parse.quote(path + "/" + mod) })
  658. a.text = mod
  659. td.append(a)
  660. tr.append(td)
  661. td = xml.etree.ElementTree.Element("td")
  662. stats_spec2xsl(stats_spec[mod], td,
  663. path + "/" + mod)
  664. tr.append(td)
  665. foreach.append(tr)
  666. table.append(foreach)
  667. xsl_elem.append(table)
  668. # assumed stats_spec
  669. else:
  670. if stats_spec['item_type'] == 'map':
  671. table = xml.etree.ElementTree.Element("table")
  672. tr = xml.etree.ElementTree.Element("tr")
  673. th = xml.etree.ElementTree.Element("th")
  674. th.text = "Item Name"
  675. tr.append(th)
  676. th = xml.etree.ElementTree.Element("th")
  677. th.text = "Item Value"
  678. tr.append(th)
  679. table.append(tr)
  680. foreach = xml.etree.ElementTree.Element(
  681. "xsl:for-each", attrib={ "select" : stats_spec['item_name'] })
  682. tr = xml.etree.ElementTree.Element("tr")
  683. td = xml.etree.ElementTree.Element(
  684. "td",
  685. attrib={ "class" : "title",
  686. "title" : stats_spec["item_description"] \
  687. if "item_description" in stats_spec \
  688. else "" })
  689. # TODO: Consider whether we should always use
  690. # the identical name "item_name" for the
  691. # user-visible name in XSL.
  692. td.text = stats_spec[ "item_title" if "item_title" in stats_spec else "item_name" ]
  693. tr.append(td)
  694. td = xml.etree.ElementTree.Element("td")
  695. stats_spec2xsl(stats_spec['map_item_spec'], td,
  696. path + "/" + stats_spec["item_name"])
  697. tr.append(td)
  698. foreach.append(tr)
  699. table.append(foreach)
  700. xsl_elem.append(table)
  701. elif stats_spec['item_type'] == 'list':
  702. stats_spec2xsl(stats_spec['list_item_spec'], xsl_elem,
  703. path + "/" + stats_spec["item_name"])
  704. else:
  705. xsl_valueof = xml.etree.ElementTree.Element(
  706. "xsl:value-of",
  707. attrib={'select': stats_spec["item_name"]})
  708. xsl_elem.append(xsl_valueof)
  709. # multiple stats_specs
  710. elif type(stats_spec) is list:
  711. table = xml.etree.ElementTree.Element("table")
  712. tr = xml.etree.ElementTree.Element("tr")
  713. th = xml.etree.ElementTree.Element("th")
  714. th.text = "Item Name"
  715. tr.append(th)
  716. th = xml.etree.ElementTree.Element("th")
  717. th.text = "Item Value"
  718. tr.append(th)
  719. table.append(tr)
  720. for item_spec in stats_spec:
  721. tr = xml.etree.ElementTree.Element("tr")
  722. td = xml.etree.ElementTree.Element(
  723. "td",
  724. attrib={ "class" : "title",
  725. "title" : item_spec["item_description"] \
  726. if "item_description" in item_spec \
  727. else "" })
  728. # if the path length is equal to or shorter than
  729. # XML_URL_PATH + /Module/Item, add the anchor tag.
  730. if len(path.split('/')) <= len((XML_URL_PATH + '/Module/Item').split('/')):
  731. a = xml.etree.ElementTree.Element(
  732. "a", attrib={ "href": urllib.parse.quote(path + "/" + item_spec["item_name"]) })
  733. a.text = item_spec[ "item_title" if "item_title" in item_spec else "item_name" ]
  734. td.append(a)
  735. else:
  736. td.text = item_spec[ "item_title" if "item_title" in item_spec else "item_name" ]
  737. tr.append(td)
  738. td = xml.etree.ElementTree.Element("td")
  739. stats_spec2xsl(item_spec, td, path)
  740. tr.append(td)
  741. if item_spec['item_type'] == 'list':
  742. foreach = xml.etree.ElementTree.Element(
  743. "xsl:for-each", attrib={ "select" : item_spec['item_name'] })
  744. foreach.append(tr)
  745. table.append(foreach)
  746. else:
  747. table.append(tr)
  748. xsl_elem.append(table)
  749. # for XSL
  750. stats_spec = self.get_stats_spec(module_name, item_name)
  751. xsd_root = xml.etree.ElementTree.Element( # started with xml:template tag
  752. "xsl:template",
  753. attrib={'match': "bind10:statistics"})
  754. stats_spec2xsl(stats_spec, xsd_root)
  755. # The coding conversion is tricky. xml..tostring() of Python 3.2
  756. # returns bytes (not string) regardless of the coding, while
  757. # tostring() of Python 3.1 returns a string. To support both
  758. # cases transparently, we first make sure tostring() returns
  759. # bytes by specifying utf-8 and then convert the result to a
  760. # plain string (code below assume it).
  761. # FIXME: Non-ASCII characters might be lost here. Consider how
  762. # the whole system should handle non-ASCII characters.
  763. xsl_string = str(xml.etree.ElementTree.tostring(xsd_root, encoding='utf-8'),
  764. encoding='us-ascii')
  765. self.xsl_body = self.open_template(XSL_TEMPLATE_LOCATION).substitute(
  766. xsl_string=xsl_string,
  767. xsd_namespace=XSD_NAMESPACE)
  768. assert self.xsl_body is not None
  769. return self.xsl_body
  770. def open_template(self, file_name):
  771. """It opens a template file, and it loads all lines to a
  772. string variable and returns string. Template object includes
  773. the variable. Limitation of a file size isn't needed there."""
  774. f = open(file_name, 'r')
  775. lines = "".join(f.readlines())
  776. f.close()
  777. assert lines is not None
  778. return string.Template(lines)
  779. if __name__ == "__main__":
  780. try:
  781. parser = OptionParser()
  782. parser.add_option(
  783. "-v", "--verbose", dest="verbose", action="store_true",
  784. help="enable maximum debug logging")
  785. (options, args) = parser.parse_args()
  786. if options.verbose:
  787. isc.log.init("b10-stats-httpd", "DEBUG", 99)
  788. stats_httpd = StatsHttpd()
  789. stats_httpd.start()
  790. except OptionValueError as ove:
  791. logger.fatal(STATHTTPD_BAD_OPTION_VALUE, ove)
  792. sys.exit(1)
  793. except isc.cc.session.SessionError as se:
  794. logger.fatal(STATHTTPD_CC_SESSION_ERROR, se)
  795. sys.exit(1)
  796. except HttpServerError as hse:
  797. logger.fatal(STATHTTPD_START_SERVER_INIT_ERROR, hse)
  798. sys.exit(1)
  799. except KeyboardInterrupt as kie:
  800. logger.info(STATHTTPD_STOPPED_BY_KEYBOARD)