stats.py.in 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  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. import sys; sys.path.append ('@@PYTHONPATH@@')
  17. import os
  18. import signal
  19. import select
  20. from time import time, strftime, gmtime
  21. from optparse import OptionParser, OptionValueError
  22. from collections import defaultdict
  23. from isc.config.ccsession import ModuleCCSession, create_answer
  24. from isc.cc import Session, SessionError
  25. # for setproctitle
  26. import isc.util.process
  27. isc.util.process.rename()
  28. # If B10_FROM_SOURCE is set in the environment, we use data files
  29. # from a directory relative to that, otherwise we use the ones
  30. # installed on the system
  31. if "B10_FROM_SOURCE" in os.environ:
  32. BASE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + \
  33. "src" + os.sep + "bin" + os.sep + "stats"
  34. else:
  35. PREFIX = "@prefix@"
  36. DATAROOTDIR = "@datarootdir@"
  37. BASE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@"
  38. BASE_LOCATION = BASE_LOCATION.replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
  39. SPECFILE_LOCATION = BASE_LOCATION + os.sep + "stats.spec"
  40. SCHEMA_SPECFILE_LOCATION = BASE_LOCATION + os.sep + "stats-schema.spec"
  41. class Singleton(type):
  42. """
  43. A abstract class of singleton pattern
  44. """
  45. # Because of singleton pattern:
  46. # At the beginning of coding, one UNIX domain socket is needed
  47. # for config manager, another socket is needed for stats module,
  48. # then stats module might need two sockets. So I adopted the
  49. # singleton pattern because I avoid creating multiple sockets in
  50. # one stats module. But in the initial version stats module
  51. # reports only via bindctl, so just one socket is needed. To use
  52. # the singleton pattern is not important now. :(
  53. def __init__(self, *args, **kwargs):
  54. type.__init__(self, *args, **kwargs)
  55. self._instances = {}
  56. def __call__(self, *args, **kwargs):
  57. if args not in self._instances:
  58. self._instances[args]={}
  59. kw = tuple(kwargs.items())
  60. if kw not in self._instances[args]:
  61. self._instances[args][kw] = type.__call__(self, *args, **kwargs)
  62. return self._instances[args][kw]
  63. class Callback():
  64. """
  65. A Callback handler class
  66. """
  67. def __init__(self, name=None, callback=None, args=(), kwargs={}):
  68. self.name = name
  69. self.callback = callback
  70. self.args = args
  71. self.kwargs = kwargs
  72. def __call__(self, *args, **kwargs):
  73. if not args:
  74. args = self.args
  75. if not kwargs:
  76. kwargs = self.kwargs
  77. if self.callback:
  78. return self.callback(*args, **kwargs)
  79. class Subject():
  80. """
  81. A abstract subject class of observer pattern
  82. """
  83. # Because of observer pattern:
  84. # In the initial release, I'm also sure that observer pattern
  85. # isn't definitely needed because the interface between gathering
  86. # and reporting statistics data is single. However in the future
  87. # release, the interfaces may be multiple, that is, multiple
  88. # listeners may be needed. For example, one interface, which
  89. # stats module has, is for between ''config manager'' and stats
  90. # module, another interface is for between ''HTTP server'' and
  91. # stats module, and one more interface is for between ''SNMP
  92. # server'' and stats module. So by considering that stats module
  93. # needs multiple interfaces in the future release, I adopted the
  94. # observer pattern in stats module. But I don't have concrete
  95. # ideas in case of multiple listener currently.
  96. def __init__(self):
  97. self._listeners = []
  98. def attach(self, listener):
  99. if not listener in self._listeners:
  100. self._listeners.append(listener)
  101. def detach(self, listener):
  102. try:
  103. self._listeners.remove(listener)
  104. except ValueError:
  105. pass
  106. def notify(self, event, modifier=None):
  107. for listener in self._listeners:
  108. if modifier != listener:
  109. listener.update(event)
  110. class Listener():
  111. """
  112. A abstract listener class of observer pattern
  113. """
  114. def __init__(self, subject):
  115. self.subject = subject
  116. self.subject.attach(self)
  117. self.events = {}
  118. def update(self, name):
  119. if name in self.events:
  120. callback = self.events[name]
  121. return callback()
  122. def add_event(self, event):
  123. self.events[event.name]=event
  124. class SessionSubject(Subject, metaclass=Singleton):
  125. """
  126. A concrete subject class which creates CC session object
  127. """
  128. def __init__(self, session=None, verbose=False):
  129. Subject.__init__(self)
  130. self.verbose = verbose
  131. self.session=session
  132. self.running = False
  133. def start(self):
  134. self.running = True
  135. self.notify('start')
  136. def stop(self):
  137. self.running = False
  138. self.notify('stop')
  139. def check(self):
  140. self.notify('check')
  141. class CCSessionListener(Listener):
  142. """
  143. A concrete listener class which creates SessionSubject object and
  144. ModuleCCSession object
  145. """
  146. def __init__(self, subject, verbose=False):
  147. Listener.__init__(self, subject)
  148. self.verbose = verbose
  149. self.session = subject.session
  150. self.boot_time = get_datetime()
  151. # create ModuleCCSession object
  152. self.cc_session = ModuleCCSession(SPECFILE_LOCATION,
  153. self.config_handler,
  154. self.command_handler,
  155. self.session)
  156. self.session = self.subject.session = self.cc_session._session
  157. # initialize internal data
  158. self.stats_spec = isc.config.module_spec_from_file(SCHEMA_SPECFILE_LOCATION).get_config_spec()
  159. self.stats_data = self.initialize_data(self.stats_spec)
  160. # add event handler invoked via SessionSubject object
  161. self.add_event(Callback('start', self.start))
  162. self.add_event(Callback('stop', self.stop))
  163. self.add_event(Callback('check', self.check))
  164. # don't add 'command_' suffix to the special commands in
  165. # order to prevent executing internal command via bindctl
  166. # get commands spec
  167. self.commands_spec = self.cc_session.get_module_spec().get_commands_spec()
  168. # add event handler related command_handler of ModuleCCSession
  169. # invoked via bindctl
  170. for cmd in self.commands_spec:
  171. try:
  172. # add prefix "command_"
  173. name = "command_" + cmd["command_name"]
  174. callback = getattr(self, name)
  175. kwargs = self.initialize_data(cmd["command_args"])
  176. self.add_event(Callback(name=name, callback=callback, args=(), kwargs=kwargs))
  177. except AttributeError as ae:
  178. sys.stderr.write("[b10-stats] Caught undefined command while parsing spec file: "
  179. +str(cmd["command_name"])+"\n")
  180. def start(self):
  181. """
  182. start the cc chanel
  183. """
  184. # set initial value
  185. self.stats_data['stats.boot_time'] = self.boot_time
  186. self.stats_data['stats.start_time'] = get_datetime()
  187. self.stats_data['stats.last_update_time'] = get_datetime()
  188. self.stats_data['stats.lname'] = self.session.lname
  189. self.cc_session.start()
  190. # request Bob to send statistics data
  191. if self.verbose:
  192. sys.stdout.write("[b10-stats] request Bob to send statistics data\n")
  193. cmd = isc.config.ccsession.create_command("sendstats", None)
  194. seq = self.session.group_sendmsg(cmd, 'Boss')
  195. self.session.group_recvmsg(True, seq)
  196. def stop(self):
  197. """
  198. stop the cc chanel
  199. """
  200. return self.cc_session.close()
  201. def check(self):
  202. """
  203. check the cc chanel
  204. """
  205. return self.cc_session.check_command(False)
  206. def config_handler(self, new_config):
  207. """
  208. handle a configure from the cc channel
  209. """
  210. if self.verbose:
  211. sys.stdout.write("[b10-stats] newconfig received: "+str(new_config)+"\n")
  212. # do nothing currently
  213. return create_answer(0)
  214. def command_handler(self, command, *args, **kwargs):
  215. """
  216. handle commands from the cc channel
  217. """
  218. # add 'command_' suffix in order to executing command via bindctl
  219. name = 'command_' + command
  220. if name in self.events:
  221. event = self.events[name]
  222. return event(*args, **kwargs)
  223. else:
  224. return self.command_unknown(command, args)
  225. def command_shutdown(self, args):
  226. """
  227. handle shutdown command
  228. """
  229. if self.verbose:
  230. sys.stdout.write("[b10-stats] 'shutdown' command received\n")
  231. self.subject.running = False
  232. return create_answer(0)
  233. def command_set(self, args, stats_data={}):
  234. """
  235. handle set command
  236. """
  237. # 'args' must be dictionary type
  238. self.stats_data.update(args['stats_data'])
  239. # overwrite "stats.LastUpdateTime"
  240. self.stats_data['stats.last_update_time'] = get_datetime()
  241. return create_answer(0)
  242. def command_remove(self, args, stats_item_name=''):
  243. """
  244. handle remove command
  245. """
  246. if self.verbose:
  247. sys.stdout.write("[b10-stats] 'remove' command received, args: "+str(args)+"\n")
  248. # 'args' must be dictionary type
  249. if args and args['stats_item_name'] in self.stats_data:
  250. stats_item_name = args['stats_item_name']
  251. # just remove one item
  252. self.stats_data.pop(stats_item_name)
  253. return create_answer(0)
  254. def command_show(self, args, stats_item_name=''):
  255. """
  256. handle show command
  257. """
  258. if self.verbose:
  259. sys.stdout.write("[b10-stats] 'show' command received, args: "+str(args)+"\n")
  260. # always overwrite 'report_time' and 'stats.timestamp'
  261. # if "show" command invoked
  262. self.stats_data['report_time'] = get_datetime()
  263. self.stats_data['stats.timestamp'] = get_timestamp()
  264. # if with args
  265. if args and args['stats_item_name'] in self.stats_data:
  266. stats_item_name = args['stats_item_name']
  267. return create_answer(0, {stats_item_name: self.stats_data[stats_item_name]})
  268. return create_answer(0, self.stats_data)
  269. def command_reset(self, args):
  270. """
  271. handle reset command
  272. """
  273. if self.verbose:
  274. sys.stdout.write("[b10-stats] 'reset' command received\n")
  275. # re-initialize internal variables
  276. self.stats_data = self.initialize_data(self.stats_spec)
  277. # reset initial value
  278. self.stats_data['stats.boot_time'] = self.boot_time
  279. self.stats_data['stats.start_time'] = get_datetime()
  280. self.stats_data['stats.last_update_time'] = get_datetime()
  281. self.stats_data['stats.lname'] = self.session.lname
  282. return create_answer(0)
  283. def command_status(self, args):
  284. """
  285. handle status command
  286. """
  287. if self.verbose:
  288. sys.stdout.write("[b10-stats] 'status' command received\n")
  289. # just return "I'm alive."
  290. return create_answer(0, "I'm alive.")
  291. def command_unknown(self, command, args):
  292. """
  293. handle an unknown command
  294. """
  295. if self.verbose:
  296. sys.stdout.write("[b10-stats] Unknown command received: '"
  297. + str(command) + "'\n")
  298. return create_answer(1, "Unknown command: '"+str(command)+"'")
  299. def initialize_data(self, spec):
  300. """
  301. initialize stats data
  302. """
  303. def __get_init_val(spec):
  304. if spec['item_type'] == 'null':
  305. return None
  306. elif spec['item_type'] == 'boolean':
  307. return bool(spec.get('item_default', False))
  308. elif spec['item_type'] == 'string':
  309. return str(spec.get('item_default', ''))
  310. elif spec['item_type'] in set(['number', 'integer']):
  311. return int(spec.get('item_default', 0))
  312. elif spec['item_type'] in set(['float', 'double', 'real']):
  313. return float(spec.get('item_default', 0.0))
  314. elif spec['item_type'] in set(['list', 'array']):
  315. return spec.get('item_default',
  316. [ __get_init_val(s) for s in spec['list_item_spec'] ])
  317. elif spec['item_type'] in set(['map', 'object']):
  318. return spec.get('item_default',
  319. dict([ (s['item_name'], __get_init_val(s)) for s in spec['map_item_spec'] ]) )
  320. else:
  321. return spec.get('item_default')
  322. return dict([ (s['item_name'], __get_init_val(s)) for s in spec ])
  323. def get_timestamp():
  324. """
  325. get current timestamp
  326. """
  327. return time()
  328. def get_datetime():
  329. """
  330. get current datetime
  331. """
  332. return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime())
  333. def main(session=None):
  334. try:
  335. parser = OptionParser()
  336. parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
  337. help="display more about what is going on")
  338. (options, args) = parser.parse_args()
  339. subject = SessionSubject(session=session, verbose=options.verbose)
  340. listener = CCSessionListener(subject, verbose=options.verbose)
  341. subject.start()
  342. while subject.running:
  343. subject.check()
  344. subject.stop()
  345. except OptionValueError:
  346. sys.stderr.write("[b10-stats] Error parsing options\n")
  347. except SessionError as se:
  348. sys.stderr.write("[b10-stats] Error creating Stats module, "
  349. + "is the command channel daemon running?\n")
  350. except KeyboardInterrupt as kie:
  351. sys.stderr.write("[b10-stats] Interrupted, exiting\n")
  352. if __name__ == "__main__":
  353. main()