stats.py.in 15 KB

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