stats.py.in 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. #!@PYTHON@
  2. # Copyright (C) 2010, 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. 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 isc
  24. import isc.util.process
  25. import isc.log
  26. from isc.log_messages.stats_messages import *
  27. isc.log.init("b10-stats")
  28. logger = isc.log.Logger("stats")
  29. # Some constants for debug levels.
  30. DBG_STATS_MESSAGING = logger.DBGLVL_COMMAND
  31. # This is for boot_time of Stats
  32. _BASETIME = gmtime()
  33. # for setproctitle
  34. isc.util.process.rename()
  35. # If B10_FROM_SOURCE is set in the environment, we use data files
  36. # from a directory relative to that, otherwise we use the ones
  37. # installed on the system
  38. if "B10_FROM_SOURCE" in os.environ:
  39. SPECFILE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + \
  40. "src" + os.sep + "bin" + os.sep + "stats" + os.sep + "stats.spec"
  41. else:
  42. PREFIX = "@prefix@"
  43. DATAROOTDIR = "@datarootdir@"
  44. SPECFILE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@" + os.sep + "stats.spec"
  45. SPECFILE_LOCATION = SPECFILE_LOCATION.replace("${datarootdir}", DATAROOTDIR)\
  46. .replace("${prefix}", PREFIX)
  47. def get_timestamp():
  48. """
  49. get current timestamp
  50. """
  51. return time()
  52. def get_datetime(gmt=None):
  53. """
  54. get current datetime
  55. """
  56. if not gmt: gmt = gmtime()
  57. return strftime("%Y-%m-%dT%H:%M:%SZ", gmt)
  58. def get_spec_defaults(spec):
  59. """
  60. extracts the default values of the items from spec specified in
  61. arg, and returns the dict-type variable which is a set of the item
  62. names and the default values
  63. """
  64. if type(spec) is not list: return {}
  65. def _get_spec_defaults(spec):
  66. item_type = spec['item_type']
  67. if item_type == "integer":
  68. return int(spec.get('item_default', 0))
  69. elif item_type == "real":
  70. return float(spec.get('item_default', 0.0))
  71. elif item_type == "boolean":
  72. return bool(spec.get('item_default', False))
  73. elif item_type == "string":
  74. return str(spec.get('item_default', ""))
  75. elif item_type == "list":
  76. return spec.get(
  77. "item_default",
  78. [ _get_spec_defaults(spec["list_item_spec"]) ])
  79. elif item_type == "map":
  80. return spec.get(
  81. "item_default",
  82. dict([ (s["item_name"], _get_spec_defaults(s)) for s in spec["map_item_spec"] ]) )
  83. else:
  84. return spec.get("item_default", None)
  85. return dict([ (s['item_name'], _get_spec_defaults(s)) for s in spec ])
  86. class Callback():
  87. """
  88. A Callback handler class
  89. """
  90. def __init__(self, command=None, args=(), kwargs={}):
  91. self.command = command
  92. self.args = args
  93. self.kwargs = kwargs
  94. def __call__(self, *args, **kwargs):
  95. if not args: args = self.args
  96. if not kwargs: kwargs = self.kwargs
  97. if self.command: return self.command(*args, **kwargs)
  98. class StatsError(Exception):
  99. """Exception class for Stats class"""
  100. pass
  101. class Stats:
  102. """
  103. Main class of stats module
  104. """
  105. def __init__(self):
  106. self.running = False
  107. # create ModuleCCSession object
  108. self.mccs = isc.config.ModuleCCSession(SPECFILE_LOCATION,
  109. self.config_handler,
  110. self.command_handler)
  111. self.cc_session = self.mccs._session
  112. # get module spec
  113. self.module_name = self.mccs.get_module_spec().get_module_name()
  114. self.modules = {}
  115. self.statistics_data = {}
  116. # get commands spec
  117. self.commands_spec = self.mccs.get_module_spec().get_commands_spec()
  118. # add event handler related command_handler of ModuleCCSession
  119. self.callbacks = {}
  120. for cmd in self.commands_spec:
  121. # add prefix "command_"
  122. name = "command_" + cmd["command_name"]
  123. try:
  124. callback = getattr(self, name)
  125. kwargs = get_spec_defaults(cmd["command_args"])
  126. self.callbacks[name] = Callback(command=callback, kwargs=kwargs)
  127. except AttributeError:
  128. raise StatsError(STATS_UNKNOWN_COMMAND_IN_SPEC, cmd["command_name"])
  129. self.mccs.start()
  130. def start(self):
  131. """
  132. Start stats module
  133. """
  134. self.running = True
  135. logger.info(STATS_STARTING)
  136. # request Bob to send statistics data
  137. logger.debug(DBG_STATS_MESSAGING, STATS_SEND_REQUEST_BOSS)
  138. cmd = isc.config.ccsession.create_command("getstats", None)
  139. seq = self.cc_session.group_sendmsg(cmd, 'Boss')
  140. try:
  141. answer, env = self.cc_session.group_recvmsg(False, seq)
  142. if answer:
  143. rcode, args = isc.config.ccsession.parse_answer(answer)
  144. if rcode == 0:
  145. errors = self.update_statistics_data(
  146. args["owner"], **args["data"])
  147. if errors:
  148. raise StatsError("boss spec file is incorrect: "
  149. + ", ".join(errors))
  150. errors = self.update_statistics_data(
  151. self.module_name,
  152. last_update_time=get_datetime())
  153. if errors:
  154. raise StatsError("stats spec file is incorrect: "
  155. + ", ".join(errors))
  156. except isc.cc.session.SessionTimeout:
  157. pass
  158. # initialized Statistics data
  159. errors = self.update_statistics_data(
  160. self.module_name,
  161. lname=self.cc_session.lname,
  162. boot_time=get_datetime(_BASETIME)
  163. )
  164. if errors:
  165. raise StatsError("stats spec file is incorrect: "
  166. + ", ".join(errors))
  167. try:
  168. while self.running:
  169. self.mccs.check_command(False)
  170. finally:
  171. self.mccs.send_stopping()
  172. def config_handler(self, new_config):
  173. """
  174. handle a configure from the cc channel
  175. """
  176. logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_NEW_CONFIG,
  177. new_config)
  178. # do nothing currently
  179. return isc.config.create_answer(0)
  180. def command_handler(self, command, kwargs):
  181. """
  182. handle commands from the cc channel
  183. """
  184. name = 'command_' + command
  185. if name in self.callbacks:
  186. callback = self.callbacks[name]
  187. if kwargs:
  188. return callback(**kwargs)
  189. else:
  190. return callback()
  191. else:
  192. logger.error(STATS_RECEIVED_UNKNOWN_COMMAND, command)
  193. return isc.config.create_answer(1, "Unknown command: '"+str(command)+"'")
  194. def update_modules(self):
  195. """
  196. updates information of each module. This method gets each
  197. module's information from the config manager and sets it into
  198. self.modules. If its getting from the config manager fails, it
  199. raises StatsError.
  200. """
  201. modules = {}
  202. seq = self.cc_session.group_sendmsg(
  203. isc.config.ccsession.create_command(
  204. isc.config.ccsession.COMMAND_GET_STATISTICS_SPEC),
  205. 'ConfigManager')
  206. (answer, env) = self.cc_session.group_recvmsg(False, seq)
  207. if answer:
  208. (rcode, value) = isc.config.ccsession.parse_answer(answer)
  209. if rcode == 0:
  210. for mod in value:
  211. spec = { "module_name" : mod }
  212. if value[mod] and type(value[mod]) is list:
  213. spec["statistics"] = value[mod]
  214. modules[mod] = isc.config.module_spec.ModuleSpec(spec)
  215. else:
  216. raise StatsError("Updating module spec fails: " + str(value))
  217. modules[self.module_name] = self.mccs.get_module_spec()
  218. self.modules = modules
  219. def get_statistics_data(self, owner=None, name=None):
  220. """
  221. returns statistics data which stats module has of each
  222. module. If it can't find specified statistics data, it raises
  223. StatsError.
  224. """
  225. self.update_statistics_data()
  226. if owner and name:
  227. try:
  228. return {owner:{name:self.statistics_data[owner][name]}}
  229. except KeyError:
  230. pass
  231. elif owner:
  232. try:
  233. return {owner: self.statistics_data[owner]}
  234. except KeyError:
  235. pass
  236. elif name:
  237. pass
  238. else:
  239. return self.statistics_data
  240. raise StatsError("No statistics data found: "
  241. + "owner: " + str(owner) + ", "
  242. + "name: " + str(name))
  243. def update_statistics_data(self, owner=None, **data):
  244. """
  245. change statistics date of specified module into specified
  246. data. It updates information of each module first, and it
  247. updates statistics data. If specified data is invalid for
  248. statistics spec of specified owner, it returns a list of error
  249. messeges. If there is no error or if neither owner nor data is
  250. specified in args, it returns None.
  251. """
  252. self.update_modules()
  253. statistics_data = {}
  254. for (name, module) in self.modules.items():
  255. value = get_spec_defaults(module.get_statistics_spec())
  256. if module.validate_statistics(True, value):
  257. statistics_data[name] = value
  258. for (name, value) in self.statistics_data.items():
  259. if name in statistics_data:
  260. statistics_data[name].update(value)
  261. else:
  262. statistics_data[name] = value
  263. self.statistics_data = statistics_data
  264. if owner and data:
  265. errors = []
  266. try:
  267. if self.modules[owner].validate_statistics(False, data, errors):
  268. self.statistics_data[owner].update(data)
  269. return
  270. except KeyError:
  271. errors.append("unknown module name: " + str(owner))
  272. return errors
  273. def command_status(self):
  274. """
  275. handle status command
  276. """
  277. logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_STATUS_COMMAND)
  278. return isc.config.create_answer(
  279. 0, "Stats is up. (PID " + str(os.getpid()) + ")")
  280. def command_shutdown(self):
  281. """
  282. handle shutdown command
  283. """
  284. logger.info(STATS_RECEIVED_SHUTDOWN_COMMAND)
  285. self.running = False
  286. return isc.config.create_answer(0)
  287. def command_show(self, owner=None, name=None):
  288. """
  289. handle show command
  290. """
  291. if owner or name:
  292. logger.debug(DBG_STATS_MESSAGING,
  293. STATS_RECEIVED_SHOW_NAME_COMMAND,
  294. str(owner)+", "+str(name))
  295. else:
  296. logger.debug(DBG_STATS_MESSAGING,
  297. STATS_RECEIVED_SHOW_ALL_COMMAND)
  298. errors = self.update_statistics_data(
  299. self.module_name,
  300. timestamp=get_timestamp(),
  301. report_time=get_datetime()
  302. )
  303. if errors:
  304. raise StatsError("stats spec file is incorrect: "
  305. + ", ".join(errors))
  306. try:
  307. return isc.config.create_answer(
  308. 0, self.get_statistics_data(owner, name))
  309. except StatsError:
  310. return isc.config.create_answer(
  311. 1, "specified arguments are incorrect: " \
  312. + "owner: " + str(owner) + ", name: " + str(name))
  313. def command_showschema(self, owner=None, name=None):
  314. """
  315. handle show command
  316. """
  317. if owner or name:
  318. logger.debug(DBG_STATS_MESSAGING,
  319. STATS_RECEIVED_SHOWSCHEMA_NAME_COMMAND,
  320. str(owner)+", "+str(name))
  321. else:
  322. logger.debug(DBG_STATS_MESSAGING,
  323. STATS_RECEIVED_SHOWSCHEMA_ALL_COMMAND)
  324. self.update_modules()
  325. schema = {}
  326. schema_byname = {}
  327. for mod in self.modules:
  328. spec = self.modules[mod].get_statistics_spec()
  329. schema_byname[mod] = {}
  330. if spec:
  331. schema[mod] = spec
  332. for item in spec:
  333. schema_byname[mod][item['item_name']] = item
  334. if owner:
  335. try:
  336. if name:
  337. return isc.config.create_answer(0, {owner:[schema_byname[owner][name]]})
  338. else:
  339. return isc.config.create_answer(0, {owner:schema[owner]})
  340. except KeyError:
  341. pass
  342. else:
  343. if name:
  344. return isc.config.create_answer(1, "module name is not specified")
  345. else:
  346. return isc.config.create_answer(0, schema)
  347. return isc.config.create_answer(
  348. 1, "specified arguments are incorrect: " \
  349. + "owner: " + str(owner) + ", name: " + str(name))
  350. def command_set(self, owner, data):
  351. """
  352. handle set command
  353. """
  354. errors = self.update_statistics_data(owner, **data)
  355. if errors:
  356. return isc.config.create_answer(
  357. 1, "errors while setting statistics data: " \
  358. + ", ".join(errors))
  359. errors = self.update_statistics_data(
  360. self.module_name, last_update_time=get_datetime() )
  361. if errors:
  362. raise StatsError("stats spec file is incorrect: "
  363. + ", ".join(errors))
  364. return isc.config.create_answer(0)
  365. if __name__ == "__main__":
  366. try:
  367. parser = OptionParser()
  368. parser.add_option(
  369. "-v", "--verbose", dest="verbose", action="store_true",
  370. help="display more about what is going on")
  371. (options, args) = parser.parse_args()
  372. if options.verbose:
  373. isc.log.init("b10-stats", "DEBUG", 99)
  374. stats = Stats()
  375. stats.start()
  376. except OptionValueError as ove:
  377. logger.fatal(STATS_BAD_OPTION_VALUE, ove)
  378. sys.exit(1)
  379. except isc.cc.session.SessionError as se:
  380. logger.fatal(STATS_CC_SESSION_ERROR, se)
  381. sys.exit(1)
  382. except StatsError as se:
  383. logger.fatal(STATS_START_ERROR, se)
  384. sys.exit(1)
  385. except KeyboardInterrupt as kie:
  386. logger.info(STATS_STOPPED_BY_KEYBOARD)