stats.py.in 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  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. # statistics data by each pid
  117. self.statistics_data_bypid = {}
  118. # get commands spec
  119. self.commands_spec = self.mccs.get_module_spec().get_commands_spec()
  120. # add event handler related command_handler of ModuleCCSession
  121. self.callbacks = {}
  122. for cmd in self.commands_spec:
  123. # add prefix "command_"
  124. name = "command_" + cmd["command_name"]
  125. try:
  126. callback = getattr(self, name)
  127. kwargs = get_spec_defaults(cmd["command_args"])
  128. self.callbacks[name] = Callback(command=callback, kwargs=kwargs)
  129. except AttributeError:
  130. raise StatsError(STATS_UNKNOWN_COMMAND_IN_SPEC, cmd["command_name"])
  131. self.mccs.start()
  132. def start(self):
  133. """
  134. Start stats module
  135. """
  136. self.running = True
  137. logger.info(STATS_STARTING)
  138. # request Bob to send statistics data
  139. logger.debug(DBG_STATS_MESSAGING, STATS_SEND_REQUEST_BOSS)
  140. cmd = isc.config.ccsession.create_command("getstats", None)
  141. seq = self.cc_session.group_sendmsg(cmd, 'Boss')
  142. try:
  143. answer, env = self.cc_session.group_recvmsg(False, seq)
  144. if answer:
  145. rcode, args = isc.config.ccsession.parse_answer(answer)
  146. if rcode == 0:
  147. errors = self.update_statistics_data(
  148. args["owner"], **args["data"])
  149. if errors:
  150. raise StatsError("boss spec file is incorrect: "
  151. + ", ".join(errors))
  152. errors = self.update_statistics_data(
  153. self.module_name,
  154. last_update_time=get_datetime())
  155. if errors:
  156. raise StatsError("stats spec file is incorrect: "
  157. + ", ".join(errors))
  158. except isc.cc.session.SessionTimeout:
  159. pass
  160. # initialized Statistics data
  161. errors = self.update_statistics_data(
  162. self.module_name,
  163. lname=self.cc_session.lname,
  164. boot_time=get_datetime(_BASETIME)
  165. )
  166. if errors:
  167. raise StatsError("stats spec file is incorrect: "
  168. + ", ".join(errors))
  169. try:
  170. while self.running:
  171. self.mccs.check_command(False)
  172. finally:
  173. self.mccs.send_stopping()
  174. def config_handler(self, new_config):
  175. """
  176. handle a configure from the cc channel
  177. """
  178. logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_NEW_CONFIG,
  179. new_config)
  180. # do nothing currently
  181. return isc.config.create_answer(0)
  182. def command_handler(self, command, kwargs):
  183. """
  184. handle commands from the cc channel
  185. """
  186. name = 'command_' + command
  187. if name in self.callbacks:
  188. callback = self.callbacks[name]
  189. if kwargs:
  190. return callback(**kwargs)
  191. else:
  192. return callback()
  193. else:
  194. logger.error(STATS_RECEIVED_UNKNOWN_COMMAND, command)
  195. return isc.config.create_answer(1, "Unknown command: '"+str(command)+"'")
  196. def update_modules(self):
  197. """
  198. updates information of each module. This method gets each
  199. module's information from the config manager and sets it into
  200. self.modules. If its getting from the config manager fails, it
  201. raises StatsError.
  202. """
  203. modules = {}
  204. seq = self.cc_session.group_sendmsg(
  205. isc.config.ccsession.create_command(
  206. isc.config.ccsession.COMMAND_GET_STATISTICS_SPEC),
  207. 'ConfigManager')
  208. (answer, env) = self.cc_session.group_recvmsg(False, seq)
  209. if answer:
  210. (rcode, value) = isc.config.ccsession.parse_answer(answer)
  211. if rcode == 0:
  212. for mod in value:
  213. spec = { "module_name" : mod }
  214. if value[mod] and type(value[mod]) is list:
  215. spec["statistics"] = value[mod]
  216. modules[mod] = isc.config.module_spec.ModuleSpec(spec)
  217. else:
  218. raise StatsError("Updating module spec fails: " + str(value))
  219. modules[self.module_name] = self.mccs.get_module_spec()
  220. self.modules = modules
  221. def get_statistics_data(self, owner=None, name=None):
  222. """
  223. returns statistics data which stats module has of each
  224. module. If it can't find specified statistics data, it raises
  225. StatsError.
  226. """
  227. self.update_statistics_data()
  228. if owner and name:
  229. try:
  230. return {owner:{name:self.statistics_data[owner][name]}}
  231. except KeyError:
  232. pass
  233. elif owner:
  234. try:
  235. return {owner: self.statistics_data[owner]}
  236. except KeyError:
  237. pass
  238. elif name:
  239. pass
  240. else:
  241. return self.statistics_data
  242. raise StatsError("No statistics data found: "
  243. + "owner: " + str(owner) + ", "
  244. + "name: " + str(name))
  245. def update_statistics_data(self, owner=None, pid=-1, **data):
  246. """
  247. change statistics date of specified module into specified
  248. data. It updates information of each module first, and it
  249. updates statistics data. If specified data is invalid for
  250. statistics spec of specified owner, it returns a list of error
  251. messages. If there is no error or if neither owner nor data is
  252. specified in args, it returns None. pid is the process id of
  253. the sender module in order for stats to identify which
  254. instance sends statistics data in the situation that multiple
  255. instances are working.
  256. """
  257. # Note:
  258. # The fix of #1751 is for multiple instances working. It is
  259. # assumed here that they send different statistics data with
  260. # each PID. Stats should save their statistics data by
  261. # PID. The statistics data, which is the existing variable, is
  262. # preserved by accumlating from statistics data by PID. This
  263. # is an ad-hoc fix because administrators can not see
  264. # statistics by each instance via bindctl or HTTP/XML. These
  265. # interfaces aren't changed in this fix.
  266. def _accum_bymodule(statistics_data_bypid):
  267. # This is an internal function for the superordinate
  268. # function. It accumulates statistics data of each PID by
  269. # module. It returns the accumulation result.
  270. def _accum(a, b):
  271. # If the first arg is dict or list type, two values
  272. # would be merged and accumlated.
  273. if type(a) is dict:
  274. return dict([ (k, _accum(v, b[k])) \
  275. if k in b else (k, v) \
  276. for (k, v) in a.items() ] \
  277. + [ (k, v) \
  278. for (k, v) in b.items() \
  279. if k not in a ])
  280. elif type(a) is list:
  281. return [ _accum(a[i], b[i]) \
  282. if len(b) > i else a[i] \
  283. for i in range(len(a)) ] \
  284. + [ b[i] \
  285. for i in range(len(b)) \
  286. if len(a) <= i ]
  287. # If the first arg is integer or float type, two
  288. # values are just added.
  289. elif type(a) is int or type(a) is float:
  290. return a + b
  291. # If the first arg is str or other types than above,
  292. # then it just returns the first arg which is assumed
  293. # to be the newer value.
  294. return a
  295. ret = {}
  296. for data in statistics_data_bypid.values():
  297. ret.update(_accum(data, ret))
  298. return ret
  299. # Firstly, it gets default statistics data in each spec file.
  300. self.update_modules()
  301. statistics_data = {}
  302. for (name, module) in self.modules.items():
  303. value = get_spec_defaults(module.get_statistics_spec())
  304. if module.validate_statistics(True, value):
  305. statistics_data[name] = value
  306. self.statistics_data = statistics_data
  307. # If the "owner" and "data" arguments in this function are
  308. # specified, then the variable of statistics data of each pid
  309. # would be updated.
  310. errors = []
  311. if owner and data:
  312. try:
  313. if self.modules[owner].validate_statistics(False, data, errors):
  314. if owner in self.statistics_data_bypid:
  315. if pid in self.statistics_data_bypid[owner]:
  316. self.statistics_data_bypid[owner][pid].update(data)
  317. else:
  318. self.statistics_data_bypid[owner][pid] = data
  319. else:
  320. self.statistics_data_bypid[owner] = { pid : data }
  321. except KeyError:
  322. errors.append("unknown module name: " + str(owner))
  323. # If there are inactive instances, which was actually running
  324. # on the system before, their statistics data would be
  325. # removed. To find inactive instances, it invokes the
  326. # "show_processes" command to Boss via the cc session. Then it
  327. # gets active instance list and compares its PIDs with PIDs in
  328. # statistics data which it already has. If inactive instances
  329. # are found, it would remove their statistics data.
  330. seq = self.cc_session.group_sendmsg(
  331. isc.config.ccsession.create_command("show_processes", None),
  332. "Boss")
  333. (answer, env) = self.cc_session.group_recvmsg(False, seq)
  334. if answer:
  335. (rcode, value) = isc.config.ccsession.parse_answer(answer)
  336. if rcode == 0:
  337. if type(value) is list and len(value) > 0 \
  338. and type(value[0]) is list and len(value[0]) > 1:
  339. mlist = [ k for k in self.statistics_data_bypid.keys() ]
  340. for m in mlist:
  341. # PID list which it has before except for -1
  342. plist1 = [ p for p in self.statistics_data_bypid[m]\
  343. .keys() if p != -1]
  344. # PID list of active instances which is
  345. # received from Boss
  346. plist2 = [ v[0] for v in value \
  347. if v[1].lower().find(m.lower()) \
  348. >= 0 ]
  349. # get inactive instance list by the difference
  350. # between plist1 and plist2
  351. nplist = set(plist1).difference(set(plist2))
  352. for p in nplist:
  353. self.statistics_data_bypid[m].pop(p)
  354. if self.statistics_data_bypid[m]:
  355. if m in self.statistics_data:
  356. self.statistics_data[m].update(
  357. _accum_bymodule(
  358. self.statistics_data_bypid[m]))
  359. # remove statistics data of the module with no
  360. # PID
  361. else:
  362. self.statistics_data_bypid.pop(m)
  363. if errors: return errors
  364. def command_status(self):
  365. """
  366. handle status command
  367. """
  368. logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_STATUS_COMMAND)
  369. return isc.config.create_answer(
  370. 0, "Stats is up. (PID " + str(os.getpid()) + ")")
  371. def command_shutdown(self, pid=None):
  372. """
  373. handle shutdown command
  374. The pid argument is ignored, it is here to match the signature.
  375. """
  376. logger.info(STATS_RECEIVED_SHUTDOWN_COMMAND)
  377. self.running = False
  378. return isc.config.create_answer(0)
  379. def command_show(self, owner=None, name=None):
  380. """
  381. handle show command
  382. """
  383. if owner or name:
  384. logger.debug(DBG_STATS_MESSAGING,
  385. STATS_RECEIVED_SHOW_NAME_COMMAND,
  386. str(owner)+", "+str(name))
  387. else:
  388. logger.debug(DBG_STATS_MESSAGING,
  389. STATS_RECEIVED_SHOW_ALL_COMMAND)
  390. errors = self.update_statistics_data(
  391. self.module_name,
  392. timestamp=get_timestamp(),
  393. report_time=get_datetime()
  394. )
  395. if errors:
  396. raise StatsError("stats spec file is incorrect: "
  397. + ", ".join(errors))
  398. try:
  399. return isc.config.create_answer(
  400. 0, self.get_statistics_data(owner, name))
  401. except StatsError:
  402. return isc.config.create_answer(
  403. 1, "specified arguments are incorrect: " \
  404. + "owner: " + str(owner) + ", name: " + str(name))
  405. def command_showschema(self, owner=None, name=None):
  406. """
  407. handle show command
  408. """
  409. if owner or name:
  410. logger.debug(DBG_STATS_MESSAGING,
  411. STATS_RECEIVED_SHOWSCHEMA_NAME_COMMAND,
  412. str(owner)+", "+str(name))
  413. else:
  414. logger.debug(DBG_STATS_MESSAGING,
  415. STATS_RECEIVED_SHOWSCHEMA_ALL_COMMAND)
  416. self.update_modules()
  417. schema = {}
  418. schema_byname = {}
  419. for mod in self.modules:
  420. spec = self.modules[mod].get_statistics_spec()
  421. schema_byname[mod] = {}
  422. if spec:
  423. schema[mod] = spec
  424. for item in spec:
  425. schema_byname[mod][item['item_name']] = item
  426. if owner:
  427. try:
  428. if name:
  429. return isc.config.create_answer(0, {owner:[schema_byname[owner][name]]})
  430. else:
  431. return isc.config.create_answer(0, {owner:schema[owner]})
  432. except KeyError:
  433. pass
  434. else:
  435. if name:
  436. return isc.config.create_answer(1, "module name is not specified")
  437. else:
  438. return isc.config.create_answer(0, schema)
  439. return isc.config.create_answer(
  440. 1, "specified arguments are incorrect: " \
  441. + "owner: " + str(owner) + ", name: " + str(name))
  442. def command_set(self, owner, pid=-1, data={}):
  443. """
  444. handle set command
  445. """
  446. errors = self.update_statistics_data(owner, pid, **data)
  447. if errors:
  448. return isc.config.create_answer(
  449. 1, "errors while setting statistics data: " \
  450. + ", ".join(errors))
  451. errors = self.update_statistics_data(
  452. self.module_name, last_update_time=get_datetime() )
  453. if errors:
  454. raise StatsError("stats spec file is incorrect: "
  455. + ", ".join(errors))
  456. return isc.config.create_answer(0)
  457. if __name__ == "__main__":
  458. try:
  459. parser = OptionParser()
  460. parser.add_option(
  461. "-v", "--verbose", dest="verbose", action="store_true",
  462. help="display more about what is going on")
  463. (options, args) = parser.parse_args()
  464. if options.verbose:
  465. isc.log.init("b10-stats", "DEBUG", 99)
  466. stats = Stats()
  467. stats.start()
  468. except OptionValueError as ove:
  469. logger.fatal(STATS_BAD_OPTION_VALUE, ove)
  470. sys.exit(1)
  471. except isc.cc.session.SessionError as se:
  472. logger.fatal(STATS_CC_SESSION_ERROR, se)
  473. sys.exit(1)
  474. except StatsError as se:
  475. logger.fatal(STATS_START_ERROR, se)
  476. sys.exit(1)
  477. except KeyboardInterrupt as kie:
  478. logger.info(STATS_STOPPED_BY_KEYBOARD)