stats.py.in 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. #!@PYTHON@
  2. # Copyright (C) 2010, 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. Statistics daemon in BIND 10
  18. """
  19. import sys; sys.path.append ('@@PYTHONPATH@@')
  20. import os
  21. from time import time, strftime, gmtime
  22. from optparse import OptionParser, OptionValueError
  23. import errno
  24. import select
  25. import isc
  26. import isc.util.process
  27. import isc.log
  28. from isc.log_messages.stats_messages import *
  29. isc.log.init("b10-stats")
  30. logger = isc.log.Logger("stats")
  31. # Some constants for debug levels.
  32. DBG_STATS_MESSAGING = logger.DBGLVL_COMMAND
  33. # This is for boot_time of Stats
  34. _BASETIME = gmtime()
  35. # for setproctitle
  36. isc.util.process.rename()
  37. # If B10_FROM_SOURCE is set in the environment, we use data files
  38. # from a directory relative to that, otherwise we use the ones
  39. # installed on the system
  40. if "B10_FROM_SOURCE" in os.environ:
  41. SPECFILE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + \
  42. "src" + os.sep + "bin" + os.sep + "stats" + os.sep + "stats.spec"
  43. else:
  44. PREFIX = "@prefix@"
  45. DATAROOTDIR = "@datarootdir@"
  46. SPECFILE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@" + os.sep + "stats.spec"
  47. SPECFILE_LOCATION = SPECFILE_LOCATION.replace("${datarootdir}", DATAROOTDIR)\
  48. .replace("${prefix}", PREFIX)
  49. def get_timestamp():
  50. """
  51. get current timestamp
  52. """
  53. return time()
  54. def get_datetime(gmt=None):
  55. """
  56. get current datetime
  57. """
  58. if not gmt: gmt = gmtime()
  59. return strftime("%Y-%m-%dT%H:%M:%SZ", gmt)
  60. def get_spec_defaults(spec):
  61. """
  62. extracts the default values of the items from spec specified in
  63. arg, and returns the dict-type variable which is a set of the item
  64. names and the default values
  65. """
  66. if type(spec) is not list: return {}
  67. def _get_spec_defaults(spec):
  68. item_type = spec['item_type']
  69. if item_type == "integer":
  70. return int(spec.get('item_default', 0))
  71. elif item_type == "real":
  72. return float(spec.get('item_default', 0.0))
  73. elif item_type == "boolean":
  74. return bool(spec.get('item_default', False))
  75. elif item_type == "string":
  76. return str(spec.get('item_default', ""))
  77. elif item_type == "list":
  78. return spec.get(
  79. "item_default",
  80. [ _get_spec_defaults(spec["list_item_spec"]) ])
  81. elif item_type == "map":
  82. return spec.get(
  83. "item_default",
  84. dict([ (s["item_name"], _get_spec_defaults(s)) for s in spec["map_item_spec"] ]) )
  85. else:
  86. return spec.get("item_default", None)
  87. return dict([ (s['item_name'], _get_spec_defaults(s)) for s in spec ])
  88. def _accum(a, b):
  89. """If the first arg is dict or list type, two values
  90. would be merged and accumlated. This is for internal use."""
  91. # If both of args are dict or list type, two
  92. # values are merged.
  93. if type(a) is dict and type(b) is dict:
  94. return dict([ (k, _accum(v, b[k])) \
  95. if k in b else (k, v) \
  96. for (k, v) in a.items() ] \
  97. + [ (k, v) \
  98. for (k, v) in b.items() \
  99. if k not in a ])
  100. elif type(a) is list and type(b) is list:
  101. return [ _accum(a[i], b[i]) \
  102. if len(b) > i else a[i] \
  103. for i in range(len(a)) ] \
  104. + [ b[i] \
  105. for i in range(len(b)) \
  106. if len(a) <= i ]
  107. # If both of args are integer or float type, two
  108. # values are added.
  109. elif (type(a) is int and type(b) is int) \
  110. or (type(a) is float or type(b) is float):
  111. return a + b
  112. # If both of args are string type,
  113. # values are compared and bigger one is returned.
  114. elif type(a) is str and type(b) is str:
  115. if a < b: return b
  116. return a
  117. # If the first arg is None type, the second value is returned.
  118. elif a is None:
  119. return b
  120. # Nothing matches above, the first arg is returned
  121. return a
  122. class Callback():
  123. """
  124. A Callback handler class
  125. """
  126. def __init__(self, command=None, args=(), kwargs={}):
  127. self.command = command
  128. self.args = args
  129. self.kwargs = kwargs
  130. def __call__(self, *args, **kwargs):
  131. if not args: args = self.args
  132. if not kwargs: kwargs = self.kwargs
  133. if self.command: return self.command(*args, **kwargs)
  134. class StatsError(Exception):
  135. """Exception class for Stats class"""
  136. pass
  137. class Stats:
  138. """
  139. Main class of stats module
  140. """
  141. def __init__(self):
  142. self.running = False
  143. # create ModuleCCSession object
  144. self.mccs = isc.config.ModuleCCSession(SPECFILE_LOCATION,
  145. self.config_handler,
  146. self.command_handler)
  147. self.cc_session = self.mccs._session
  148. # get module spec
  149. self.module_name = self.mccs.get_module_spec().get_module_name()
  150. self.modules = {}
  151. self.statistics_data = {}
  152. # statistics data by each mid
  153. self.statistics_data_bymid = {}
  154. # get commands spec
  155. self.commands_spec = self.mccs.get_module_spec().get_commands_spec()
  156. # add event handler related command_handler of ModuleCCSession
  157. self.callbacks = {}
  158. for cmd in self.commands_spec:
  159. # add prefix "command_"
  160. name = "command_" + cmd["command_name"]
  161. try:
  162. callback = getattr(self, name)
  163. kwargs = get_spec_defaults(cmd["command_args"])
  164. self.callbacks[name] = Callback(command=callback, kwargs=kwargs)
  165. except AttributeError:
  166. raise StatsError(STATS_UNKNOWN_COMMAND_IN_SPEC, cmd["command_name"])
  167. self.config = {}
  168. self.mccs.start()
  169. # setup my config
  170. self.config = dict([
  171. (itm['item_name'], self.mccs.get_value(itm['item_name'])[0])
  172. for itm in self.mccs.get_module_spec().get_config_spec()
  173. ])
  174. # set a absolute timestamp polling at next time
  175. self.next_polltime = get_timestamp() + self.get_interval()
  176. # initialized Statistics data
  177. self.update_modules()
  178. if self.update_statistics_data(
  179. self.module_name,
  180. self.cc_session.lname,
  181. {'lname': self.cc_session.lname,
  182. 'boot_time': get_datetime(_BASETIME),
  183. 'last_update_time': get_datetime()}):
  184. logger.warn(STATS_RECEIVED_INVALID_STATISTICS_DATA,
  185. self.module_name)
  186. # define the variable of the last time of polling
  187. self._lasttime_poll = 0.0
  188. def get_interval(self):
  189. """return the current value of 'poll-interval'"""
  190. return self.config['poll-interval']
  191. def do_polling(self):
  192. """Polls modules for statistics data. Return nothing. First
  193. search multiple instances of same module. Second requests
  194. each module to invoke 'getstats'. Finally updates internal
  195. statistics data every time it gets from each instance."""
  196. # It counts the number of instances of same module by
  197. # examining the third value from the array result of
  198. # 'show_processes' of Boss
  199. seq = self.cc_session.group_sendmsg(
  200. isc.config.ccsession.create_command("show_processes"),
  201. 'Boss')
  202. (answer, env) = self.cc_session.group_recvmsg(False, seq)
  203. modules = []
  204. if answer:
  205. (rcode, value) = isc.config.ccsession.parse_answer(answer)
  206. if rcode == 0 and type(value) is list:
  207. # NOTE: For example, the "show_processes" command
  208. # of Boss is assumed to return the response in this
  209. # format:
  210. # [
  211. # ...
  212. # [
  213. # 20061,
  214. # "b10-auth",
  215. # "Auth"
  216. # ],
  217. # [
  218. # 20103,
  219. # "b10-auth-2",
  220. # "Auth"
  221. # ]
  222. # ...
  223. # ]
  224. # If multiple instances of the same module are
  225. # running, the address names of them, which are at the
  226. # third element, must be also same. Thus, the value of
  227. # the third element of each outer element is read here
  228. # for counting multiple instances. This is a
  229. # workaround for counting the instances. This should
  230. # be fixed in another proper way in the future
  231. # release.
  232. modules = [ v[2] if type(v) is list and len(v) > 2 \
  233. else None for v in value ]
  234. # start requesting each module to collect statistics data
  235. sequences = []
  236. for (module_name, data) in self.get_statistics_data().items():
  237. # skip if module_name is 'Stats'
  238. if module_name == self.module_name:
  239. continue
  240. logger.debug(DBG_STATS_MESSAGING, STATS_SEND_STATISTICS_REQUEST,
  241. module_name)
  242. cmd = isc.config.ccsession.create_command(
  243. "getstats", None) # no argument
  244. seq = self.cc_session.group_sendmsg(cmd, module_name)
  245. sequences.append((module_name, seq))
  246. cnt = modules.count(module_name)
  247. if cnt > 1:
  248. sequences = sequences + [ (module_name, seq) \
  249. for i in range(cnt-1) ]
  250. # start receiving statistics data
  251. _statistics_data = []
  252. while len(sequences) > 0:
  253. try:
  254. (module_name, seq) = sequences.pop(0)
  255. answer, env = self.cc_session.group_recvmsg(
  256. False, seq)
  257. if answer:
  258. rcode, args = isc.config.ccsession.parse_answer(
  259. answer)
  260. if rcode == 0:
  261. _statistics_data.append(
  262. (module_name, env['from'], args))
  263. # skip this module if SessionTimeout raised
  264. except isc.cc.session.SessionTimeout:
  265. pass
  266. # update statistics data
  267. self.update_modules()
  268. while len(_statistics_data) > 0:
  269. (_module_name, _lname, _args) = _statistics_data.pop(0)
  270. if self.update_statistics_data(_module_name, _lname, _args):
  271. logger.warn(
  272. STATS_RECEIVED_INVALID_STATISTICS_DATA,
  273. _module_name)
  274. else:
  275. if self.update_statistics_data(
  276. self.module_name,
  277. self.cc_session.lname,
  278. {'last_update_time': get_datetime()}):
  279. logger.warn(
  280. STATS_RECEIVED_INVALID_STATISTICS_DATA,
  281. self.module_name)
  282. # if successfully done, set the last time of polling
  283. self._lasttime_poll = get_timestamp()
  284. def start(self):
  285. """
  286. Start stats module
  287. """
  288. logger.info(STATS_STARTING)
  289. def _check_command(nonblock=False):
  290. """check invoked command by waiting for 'poll-interval'
  291. seconds"""
  292. # backup original timeout
  293. orig_timeout = self.cc_session.get_timeout()
  294. # set cc-session timeout to half of a second(500ms)
  295. self.cc_session.set_timeout(500)
  296. try:
  297. answer, env = self.cc_session.group_recvmsg(nonblock)
  298. self.mccs.check_command_without_recvmsg(answer, env)
  299. except isc.cc.session.SessionTimeout:
  300. pass # waited for poll-interval seconds
  301. # restore timeout
  302. self.cc_session.set_timeout(orig_timeout)
  303. try:
  304. self.running = True
  305. while self.running:
  306. _check_command()
  307. now = get_timestamp()
  308. intval = self.get_interval()
  309. if intval > 0 and now >= self.next_polltime:
  310. # decide the next polling timestamp
  311. self.next_polltime += intval
  312. # adjust next time
  313. if self.next_polltime < now:
  314. self.next_polltime = now
  315. self.do_polling()
  316. finally:
  317. self.mccs.send_stopping()
  318. def config_handler(self, new_config):
  319. """
  320. handle a configure from the cc channel
  321. """
  322. logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_NEW_CONFIG,
  323. new_config)
  324. errors = []
  325. if not self.mccs.get_module_spec().\
  326. validate_config(False, new_config, errors):
  327. return isc.config.ccsession.create_answer(
  328. 1, ", ".join(errors))
  329. if 'poll-interval' in new_config \
  330. and new_config['poll-interval'] < 0:
  331. return isc.config.ccsession.create_answer(
  332. 1, "Negative integer ignored")
  333. self.config.update(new_config)
  334. if 'poll-interval' in self.config:
  335. # update next polling timestamp
  336. self.next_polltime = get_timestamp() + self.get_interval()
  337. return isc.config.create_answer(0)
  338. def command_handler(self, command, kwargs):
  339. """
  340. handle commands from the cc channel
  341. """
  342. name = 'command_' + command
  343. if name in self.callbacks:
  344. callback = self.callbacks[name]
  345. if kwargs:
  346. return callback(**kwargs)
  347. else:
  348. return callback()
  349. else:
  350. logger.error(STATS_RECEIVED_UNKNOWN_COMMAND, command)
  351. return isc.config.create_answer(1, "Unknown command: '"+str(command)+"'")
  352. def update_modules(self):
  353. """
  354. updates information of each module. This method gets each
  355. module's information from the config manager and sets it into
  356. self.modules. If its getting from the config manager fails, it
  357. raises StatsError.
  358. """
  359. modules = {}
  360. seq = self.cc_session.group_sendmsg(
  361. isc.config.ccsession.create_command(
  362. isc.config.ccsession.COMMAND_GET_STATISTICS_SPEC),
  363. 'ConfigManager')
  364. (answer, env) = self.cc_session.group_recvmsg(False, seq)
  365. if answer:
  366. (rcode, value) = isc.config.ccsession.parse_answer(answer)
  367. if rcode == 0:
  368. for mod in value:
  369. spec = { "module_name" : mod }
  370. if value[mod] and type(value[mod]) is list:
  371. spec["statistics"] = value[mod]
  372. modules[mod] = isc.config.module_spec.ModuleSpec(spec)
  373. else:
  374. raise StatsError("Updating module spec fails: " + str(value))
  375. modules[self.module_name] = self.mccs.get_module_spec()
  376. self.modules = modules
  377. def get_statistics_data(self, owner=None, name=None):
  378. """
  379. returns statistics data which stats module has of each
  380. module. If it can't find specified statistics data, it raises
  381. StatsError.
  382. """
  383. self.update_modules()
  384. if owner and name:
  385. try:
  386. return {owner:{name:self.statistics_data[owner][name]}}
  387. except KeyError:
  388. pass
  389. elif owner:
  390. try:
  391. return {owner: self.statistics_data[owner]}
  392. except KeyError:
  393. pass
  394. elif name:
  395. pass
  396. else:
  397. return self.statistics_data
  398. raise StatsError("No statistics data found: "
  399. + "owner: " + str(owner) + ", "
  400. + "name: " + str(name))
  401. def update_statistics_data(self, owner=None, mid=None, data=None):
  402. """
  403. change statistics data of specified module into specified
  404. data. It updates information of each module first, and it
  405. updates statistics data. If specified data is invalid for
  406. statistics spec of specified owner, it returns a list of error
  407. messages. If there is no error or if neither owner nor data is
  408. specified in args, it returns None. The 'mid' argument is an identifier of
  409. the sender module in order for stats to identify which
  410. instance sends statistics data in the situation that multiple
  411. instances are working.
  412. """
  413. # Note:
  414. # The fix of #1751 is for multiple instances working. It is
  415. # assumed here that they send different statistics data with
  416. # each sender module id (mid). Stats should save their statistics data by
  417. # mid. The statistics data, which is the existing variable, is
  418. # preserved by accumlating from statistics data by the mid. This
  419. # is an ad-hoc fix because administrators can not see
  420. # statistics by each instance via bindctl or HTTP/XML. These
  421. # interfaces aren't changed in this fix.
  422. def _accum_bymodule(statistics_data_bymid):
  423. """This is an internal method for the superordinate
  424. method. It accumulates statistics data of each module id
  425. by module. It returns a accumulated result."""
  426. # FIXME: A issue might happen when consolidating
  427. # statistics of the multiple instances. If they have
  428. # different statistics data which are not for adding each
  429. # other, this might happen: If these are integer or float,
  430. # these are added each other. If these are string , these
  431. # are compared and consolidated into bigger one. If one
  432. # of them is None type , these might be consolidated
  433. # into not None-type one. Otherwise these are overwritten
  434. # into one of them.
  435. ret = {}
  436. for data in statistics_data_bymid.values():
  437. ret.update(_accum(data, ret))
  438. return ret
  439. # Firstly, it gets default statistics data in each spec file.
  440. statistics_data = {}
  441. for (name, module) in self.modules.items():
  442. value = get_spec_defaults(module.get_statistics_spec())
  443. if module.validate_statistics(True, value):
  444. statistics_data[name] = value
  445. self.statistics_data = statistics_data
  446. # If the "owner" and "data" arguments in this function are
  447. # specified, then the variable of statistics data of each module id
  448. # would be updated.
  449. errors = []
  450. if owner and data:
  451. try:
  452. if self.modules[owner].validate_statistics(False, data, errors):
  453. if owner in self.statistics_data_bymid:
  454. if mid in self.statistics_data_bymid[owner]:
  455. self.statistics_data_bymid[owner][mid].update(data)
  456. else:
  457. self.statistics_data_bymid[owner][mid] = data
  458. else:
  459. self.statistics_data_bymid[owner] = { mid : data }
  460. except KeyError:
  461. errors.append("unknown module name: " + str(owner))
  462. # Just consolidate statistics data of each module without
  463. # removing that of modules which have been already dead
  464. mlist = [ k for k in self.statistics_data_bymid.keys() ]
  465. for m in mlist:
  466. if self.statistics_data_bymid[m]:
  467. if m in self.statistics_data:
  468. self.statistics_data[m].update(
  469. _accum_bymodule(
  470. self.statistics_data_bymid[m]))
  471. if errors: return errors
  472. def command_status(self):
  473. """
  474. handle status command
  475. """
  476. logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_STATUS_COMMAND)
  477. return isc.config.create_answer(
  478. 0, "Stats is up. (PID " + str(os.getpid()) + ")")
  479. def command_shutdown(self, pid=None):
  480. """
  481. handle shutdown command
  482. The pid argument is ignored, it is here to match the signature.
  483. """
  484. logger.info(STATS_RECEIVED_SHUTDOWN_COMMAND)
  485. self.running = False
  486. return isc.config.create_answer(0)
  487. def command_show(self, owner=None, name=None):
  488. """
  489. handle show command
  490. """
  491. # decide if polling should be done based on the the last time of
  492. # polling. If more than one second has passed since the last
  493. # request to each module, the stats module requests each module
  494. # statistics data and then shows the latest result. Otherwise,
  495. # the stats module just shows statistics data which it has.
  496. if get_timestamp() - self._lasttime_poll > 1.0:
  497. self.do_polling()
  498. if owner or name:
  499. logger.debug(DBG_STATS_MESSAGING,
  500. STATS_RECEIVED_SHOW_NAME_COMMAND,
  501. str(owner)+", "+str(name))
  502. else:
  503. logger.debug(DBG_STATS_MESSAGING,
  504. STATS_RECEIVED_SHOW_ALL_COMMAND)
  505. errors = self.update_statistics_data(
  506. self.module_name,
  507. self.cc_session.lname,
  508. {'timestamp': get_timestamp(),
  509. 'report_time': get_datetime()}
  510. )
  511. if errors:
  512. raise StatsError("stats spec file is incorrect: "
  513. + ", ".join(errors))
  514. try:
  515. return isc.config.create_answer(
  516. 0, self.get_statistics_data(owner, name))
  517. except StatsError:
  518. return isc.config.create_answer(
  519. 1, "specified arguments are incorrect: " \
  520. + "owner: " + str(owner) + ", name: " + str(name))
  521. def command_showschema(self, owner=None, name=None):
  522. """
  523. handle show command
  524. """
  525. if owner or name:
  526. logger.debug(DBG_STATS_MESSAGING,
  527. STATS_RECEIVED_SHOWSCHEMA_NAME_COMMAND,
  528. str(owner)+", "+str(name))
  529. else:
  530. logger.debug(DBG_STATS_MESSAGING,
  531. STATS_RECEIVED_SHOWSCHEMA_ALL_COMMAND)
  532. self.update_modules()
  533. schema = {}
  534. schema_byname = {}
  535. for mod in self.modules:
  536. spec = self.modules[mod].get_statistics_spec()
  537. schema_byname[mod] = {}
  538. if spec:
  539. schema[mod] = spec
  540. for item in spec:
  541. schema_byname[mod][item['item_name']] = item
  542. if owner:
  543. try:
  544. if name:
  545. return isc.config.create_answer(0, {owner:[schema_byname[owner][name]]})
  546. else:
  547. return isc.config.create_answer(0, {owner:schema[owner]})
  548. except KeyError:
  549. pass
  550. else:
  551. if name:
  552. return isc.config.create_answer(1, "module name is not specified")
  553. else:
  554. return isc.config.create_answer(0, schema)
  555. return isc.config.create_answer(
  556. 1, "specified arguments are incorrect: " \
  557. + "owner: " + str(owner) + ", name: " + str(name))
  558. if __name__ == "__main__":
  559. try:
  560. parser = OptionParser()
  561. parser.add_option(
  562. "-v", "--verbose", dest="verbose", action="store_true",
  563. help="enable maximum debug logging")
  564. (options, args) = parser.parse_args()
  565. if options.verbose:
  566. isc.log.init("b10-stats", "DEBUG", 99)
  567. stats = Stats()
  568. stats.start()
  569. except OptionValueError as ove:
  570. logger.fatal(STATS_BAD_OPTION_VALUE, ove)
  571. sys.exit(1)
  572. except isc.cc.session.SessionError as se:
  573. logger.fatal(STATS_CC_SESSION_ERROR, se)
  574. sys.exit(1)
  575. except StatsError as se:
  576. logger.fatal(STATS_START_ERROR, se)
  577. sys.exit(1)
  578. except KeyboardInterrupt as kie:
  579. logger.info(STATS_STOPPED_BY_KEYBOARD)