#!@PYTHON@ # Copyright (C) 2010, 2011 Internet Systems Consortium. # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ Statistics daemon in BIND 10 """ import sys; sys.path.append ('@@PYTHONPATH@@') import os from time import time, strftime, gmtime from optparse import OptionParser, OptionValueError import isc import isc.util.process import isc.log from isc.log_messages.stats_messages import * isc.log.init("b10-stats") logger = isc.log.Logger("stats") # Some constants for debug levels. DBG_STATS_MESSAGING = logger.DBGLVL_COMMAND # This is for boot_time of Stats _BASETIME = gmtime() # for setproctitle isc.util.process.rename() # If B10_FROM_SOURCE is set in the environment, we use data files # from a directory relative to that, otherwise we use the ones # installed on the system if "B10_FROM_SOURCE" in os.environ: SPECFILE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + \ "src" + os.sep + "bin" + os.sep + "stats" + os.sep + "stats.spec" else: PREFIX = "@prefix@" DATAROOTDIR = "@datarootdir@" SPECFILE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@" + os.sep + "stats.spec" SPECFILE_LOCATION = SPECFILE_LOCATION.replace("${datarootdir}", DATAROOTDIR)\ .replace("${prefix}", PREFIX) def get_timestamp(): """ get current timestamp """ return time() def get_datetime(gmt=None): """ get current datetime """ if not gmt: gmt = gmtime() return strftime("%Y-%m-%dT%H:%M:%SZ", gmt) def get_spec_defaults(spec): """ extracts the default values of the items from spec specified in arg, and returns the dict-type variable which is a set of the item names and the default values """ if type(spec) is not list: return {} def _get_spec_defaults(spec): item_type = spec['item_type'] if item_type == "integer": return int(spec.get('item_default', 0)) elif item_type == "real": return float(spec.get('item_default', 0.0)) elif item_type == "boolean": return bool(spec.get('item_default', False)) elif item_type == "string": return str(spec.get('item_default', "")) elif item_type == "list": return spec.get( "item_default", [ _get_spec_defaults(spec["list_item_spec"]) ]) elif item_type == "map": return spec.get( "item_default", dict([ (s["item_name"], _get_spec_defaults(s)) for s in spec["map_item_spec"] ]) ) else: return spec.get("item_default", None) return dict([ (s['item_name'], _get_spec_defaults(s)) for s in spec ]) class Callback(): """ A Callback handler class """ def __init__(self, command=None, args=(), kwargs={}): self.command = command self.args = args self.kwargs = kwargs def __call__(self, *args, **kwargs): if not args: args = self.args if not kwargs: kwargs = self.kwargs if self.command: return self.command(*args, **kwargs) class StatsError(Exception): """Exception class for Stats class""" pass class Stats: """ Main class of stats module """ def __init__(self): self.running = False # create ModuleCCSession object self.mccs = isc.config.ModuleCCSession(SPECFILE_LOCATION, self.config_handler, self.command_handler) self.cc_session = self.mccs._session # get module spec self.module_name = self.mccs.get_module_spec().get_module_name() self.modules = {} self.statistics_data = {} # get commands spec self.commands_spec = self.mccs.get_module_spec().get_commands_spec() # add event handler related command_handler of ModuleCCSession self.callbacks = {} for cmd in self.commands_spec: # add prefix "command_" name = "command_" + cmd["command_name"] try: callback = getattr(self, name) kwargs = get_spec_defaults(cmd["command_args"]) self.callbacks[name] = Callback(command=callback, kwargs=kwargs) except AttributeError: raise StatsError(STATS_UNKNOWN_COMMAND_IN_SPEC, cmd["command_name"]) self.mccs.start() def start(self): """ Start stats module """ self.running = True logger.info(STATS_STARTING) # request Bob to send statistics data logger.debug(DBG_STATS_MESSAGING, STATS_SEND_REQUEST_BOSS) cmd = isc.config.ccsession.create_command("getstats", None) seq = self.cc_session.group_sendmsg(cmd, 'Boss') try: answer, env = self.cc_session.group_recvmsg(False, seq) if answer: rcode, args = isc.config.ccsession.parse_answer(answer) if rcode == 0: errors = self.update_statistics_data( args["owner"], **args["data"]) if errors: raise StatsError("boss spec file is incorrect: " + ", ".join(errors)) errors = self.update_statistics_data( self.module_name, last_update_time=get_datetime()) if errors: raise StatsError("stats spec file is incorrect: " + ", ".join(errors)) except isc.cc.session.SessionTimeout: pass # initialized Statistics data errors = self.update_statistics_data( self.module_name, lname=self.cc_session.lname, boot_time=get_datetime(_BASETIME) ) if errors: raise StatsError("stats spec file is incorrect: " + ", ".join(errors)) try: while self.running: self.mccs.check_command(False) finally: self.mccs.send_stopping() def config_handler(self, new_config): """ handle a configure from the cc channel """ logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_NEW_CONFIG, new_config) # do nothing currently return isc.config.create_answer(0) def command_handler(self, command, kwargs): """ handle commands from the cc channel """ name = 'command_' + command if name in self.callbacks: callback = self.callbacks[name] if kwargs: return callback(**kwargs) else: return callback() else: logger.error(STATS_RECEIVED_UNKNOWN_COMMAND, command) return isc.config.create_answer(1, "Unknown command: '"+str(command)+"'") def update_modules(self): """ updates information of each module. This method gets each module's information from the config manager and sets it into self.modules. If its getting from the config manager fails, it raises StatsError. """ modules = {} seq = self.cc_session.group_sendmsg( isc.config.ccsession.create_command( isc.config.ccsession.COMMAND_GET_STATISTICS_SPEC), 'ConfigManager') (answer, env) = self.cc_session.group_recvmsg(False, seq) if answer: (rcode, value) = isc.config.ccsession.parse_answer(answer) if rcode == 0: for mod in value: spec = { "module_name" : mod } if value[mod] and type(value[mod]) is list: spec["statistics"] = value[mod] modules[mod] = isc.config.module_spec.ModuleSpec(spec) else: raise StatsError("Updating module spec fails: " + str(value)) modules[self.module_name] = self.mccs.get_module_spec() self.modules = modules def get_statistics_data(self, owner=None, name=None): """ returns statistics data which stats module has of each module. If it can't find specified statistics data, it raises StatsError. """ self.update_statistics_data() if owner and name: try: return {owner:{name:self.statistics_data[owner][name]}} except KeyError: pass elif owner: try: return {owner: self.statistics_data[owner]} except KeyError: pass elif name: pass else: return self.statistics_data raise StatsError("No statistics data found: " + "owner: " + str(owner) + ", " + "name: " + str(name)) def update_statistics_data(self, owner=None, **data): """ change statistics date of specified module into specified data. It updates information of each module first, and it updates statistics data. If specified data is invalid for statistics spec of specified owner, it returns a list of error messeges. If there is no error or if neither owner nor data is specified in args, it returns None. """ self.update_modules() statistics_data = {} for (name, module) in self.modules.items(): value = get_spec_defaults(module.get_statistics_spec()) if module.validate_statistics(True, value): statistics_data[name] = value for (name, value) in self.statistics_data.items(): if name in statistics_data: statistics_data[name].update(value) else: statistics_data[name] = value self.statistics_data = statistics_data if owner and data: errors = [] try: if self.modules[owner].validate_statistics(False, data, errors): self.statistics_data[owner].update(data) return except KeyError: errors.append("unknown module name: " + str(owner)) return errors def command_status(self): """ handle status command """ logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_STATUS_COMMAND) return isc.config.create_answer( 0, "Stats is up. (PID " + str(os.getpid()) + ")") def command_shutdown(self): """ handle shutdown command """ logger.info(STATS_RECEIVED_SHUTDOWN_COMMAND) self.running = False return isc.config.create_answer(0) def command_show(self, owner=None, name=None): """ handle show command """ if owner or name: logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_SHOW_NAME_COMMAND, str(owner)+", "+str(name)) else: logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_SHOW_ALL_COMMAND) errors = self.update_statistics_data( self.module_name, timestamp=get_timestamp(), report_time=get_datetime() ) if errors: raise StatsError("stats spec file is incorrect: " + ", ".join(errors)) try: return isc.config.create_answer( 0, self.get_statistics_data(owner, name)) except StatsError: return isc.config.create_answer( 1, "specified arguments are incorrect: " \ + "owner: " + str(owner) + ", name: " + str(name)) def command_showschema(self, owner=None, name=None): """ handle show command """ if owner or name: logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_SHOWSCHEMA_NAME_COMMAND, str(owner)+", "+str(name)) else: logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_SHOWSCHEMA_ALL_COMMAND) self.update_modules() schema = {} schema_byname = {} for mod in self.modules: spec = self.modules[mod].get_statistics_spec() schema_byname[mod] = {} if spec: schema[mod] = spec for item in spec: schema_byname[mod][item['item_name']] = item if owner: try: if name: return isc.config.create_answer(0, {owner:[schema_byname[owner][name]]}) else: return isc.config.create_answer(0, {owner:schema[owner]}) except KeyError: pass else: if name: return isc.config.create_answer(1, "module name is not specified") else: return isc.config.create_answer(0, schema) return isc.config.create_answer( 1, "specified arguments are incorrect: " \ + "owner: " + str(owner) + ", name: " + str(name)) def command_set(self, owner, data): """ handle set command """ errors = self.update_statistics_data(owner, **data) if errors: return isc.config.create_answer( 1, "errors while setting statistics data: " \ + ", ".join(errors)) errors = self.update_statistics_data( self.module_name, last_update_time=get_datetime() ) if errors: raise StatsError("stats spec file is incorrect: " + ", ".join(errors)) return isc.config.create_answer(0) if __name__ == "__main__": try: parser = OptionParser() parser.add_option( "-v", "--verbose", dest="verbose", action="store_true", help="display more about what is going on") (options, args) = parser.parse_args() if options.verbose: isc.log.init("b10-stats", "DEBUG", 99) stats = Stats() stats.start() except OptionValueError as ove: logger.fatal(STATS_BAD_OPTION_VALUE, ove) sys.exit(1) except isc.cc.session.SessionError as se: logger.fatal(STATS_CC_SESSION_ERROR, se) sys.exit(1) except StatsError as se: logger.fatal(STATS_START_ERROR, se) sys.exit(1) except KeyboardInterrupt as kie: logger.info(STATS_STOPPED_BY_KEYBOARD)