diff --git a/src/bin/stats/stats.py.in b/src/bin/stats/stats.py.in index ce3d9f4612..3faa3059a0 100644 --- a/src/bin/stats/stats.py.in +++ b/src/bin/stats/stats.py.in @@ -15,16 +15,17 @@ # 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 -import signal -import select from time import time, strftime, gmtime from optparse import OptionParser, OptionValueError -from collections import defaultdict -from isc.config.ccsession import ModuleCCSession, create_answer -from isc.cc import Session, SessionError +import isc +import isc.util.process import isc.log from stats_messages import * @@ -35,352 +36,24 @@ logger = isc.log.Logger("stats") # have #1074 DBG_STATS_MESSAGING = 30 +# This is for boot_time of Stats +_BASETIME = gmtime() + # for setproctitle -import isc.util.process 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: - BASE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + \ - "src" + os.sep + "bin" + os.sep + "stats" + 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@" - BASE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@" - BASE_LOCATION = BASE_LOCATION.replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX) -SPECFILE_LOCATION = BASE_LOCATION + os.sep + "stats.spec" -SCHEMA_SPECFILE_LOCATION = BASE_LOCATION + os.sep + "stats-schema.spec" - -class Singleton(type): - """ - A abstract class of singleton pattern - """ - # Because of singleton pattern: - # At the beginning of coding, one UNIX domain socket is needed - # for config manager, another socket is needed for stats module, - # then stats module might need two sockets. So I adopted the - # singleton pattern because I avoid creating multiple sockets in - # one stats module. But in the initial version stats module - # reports only via bindctl, so just one socket is needed. To use - # the singleton pattern is not important now. :( - - def __init__(self, *args, **kwargs): - type.__init__(self, *args, **kwargs) - self._instances = {} - - def __call__(self, *args, **kwargs): - if args not in self._instances: - self._instances[args]={} - kw = tuple(kwargs.items()) - if kw not in self._instances[args]: - self._instances[args][kw] = type.__call__(self, *args, **kwargs) - return self._instances[args][kw] - -class Callback(): - """ - A Callback handler class - """ - def __init__(self, name=None, callback=None, args=(), kwargs={}): - self.name = name - self.callback = callback - 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.callback: - return self.callback(*args, **kwargs) - -class Subject(): - """ - A abstract subject class of observer pattern - """ - # Because of observer pattern: - # In the initial release, I'm also sure that observer pattern - # isn't definitely needed because the interface between gathering - # and reporting statistics data is single. However in the future - # release, the interfaces may be multiple, that is, multiple - # listeners may be needed. For example, one interface, which - # stats module has, is for between ''config manager'' and stats - # module, another interface is for between ''HTTP server'' and - # stats module, and one more interface is for between ''SNMP - # server'' and stats module. So by considering that stats module - # needs multiple interfaces in the future release, I adopted the - # observer pattern in stats module. But I don't have concrete - # ideas in case of multiple listener currently. - - def __init__(self): - self._listeners = [] - - def attach(self, listener): - if not listener in self._listeners: - self._listeners.append(listener) - - def detach(self, listener): - try: - self._listeners.remove(listener) - except ValueError: - pass - - def notify(self, event, modifier=None): - for listener in self._listeners: - if modifier != listener: - listener.update(event) - -class Listener(): - """ - A abstract listener class of observer pattern - """ - def __init__(self, subject): - self.subject = subject - self.subject.attach(self) - self.events = {} - - def update(self, name): - if name in self.events: - callback = self.events[name] - return callback() - - def add_event(self, event): - self.events[event.name]=event - -class SessionSubject(Subject, metaclass=Singleton): - """ - A concrete subject class which creates CC session object - """ - def __init__(self, session=None): - Subject.__init__(self) - self.session=session - self.running = False - - def start(self): - self.running = True - self.notify('start') - - def stop(self): - self.running = False - self.notify('stop') - - def check(self): - self.notify('check') - -class CCSessionListener(Listener): - """ - A concrete listener class which creates SessionSubject object and - ModuleCCSession object - """ - def __init__(self, subject): - Listener.__init__(self, subject) - self.session = subject.session - self.boot_time = get_datetime() - - # create ModuleCCSession object - self.cc_session = ModuleCCSession(SPECFILE_LOCATION, - self.config_handler, - self.command_handler, - self.session) - - self.session = self.subject.session = self.cc_session._session - - # initialize internal data - self.stats_spec = isc.config.module_spec_from_file(SCHEMA_SPECFILE_LOCATION).get_config_spec() - self.stats_data = self.initialize_data(self.stats_spec) - - # add event handler invoked via SessionSubject object - self.add_event(Callback('start', self.start)) - self.add_event(Callback('stop', self.stop)) - self.add_event(Callback('check', self.check)) - # don't add 'command_' suffix to the special commands in - # order to prevent executing internal command via bindctl - - # get commands spec - self.commands_spec = self.cc_session.get_module_spec().get_commands_spec() - - # add event handler related command_handler of ModuleCCSession - # invoked via bindctl - for cmd in self.commands_spec: - try: - # add prefix "command_" - name = "command_" + cmd["command_name"] - callback = getattr(self, name) - kwargs = self.initialize_data(cmd["command_args"]) - self.add_event(Callback(name=name, callback=callback, args=(), kwargs=kwargs)) - except AttributeError as ae: - logger.error(STATS_UNKNOWN_COMMAND_IN_SPEC, cmd["command_name"]) - - def start(self): - """ - start the cc chanel - """ - # set initial value - self.stats_data['stats.boot_time'] = self.boot_time - self.stats_data['stats.start_time'] = get_datetime() - self.stats_data['stats.last_update_time'] = get_datetime() - self.stats_data['stats.lname'] = self.session.lname - self.cc_session.start() - # request Bob to send statistics data - logger.debug(DBG_STATS_MESSAGING, STATS_SEND_REQUEST_BOSS) - cmd = isc.config.ccsession.create_command("sendstats", None) - seq = self.session.group_sendmsg(cmd, 'Boss') - self.session.group_recvmsg(True, seq) - - def stop(self): - """ - stop the cc chanel - """ - return self.cc_session.close() - - def check(self): - """ - check the cc chanel - """ - return self.cc_session.check_command(False) - - 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 create_answer(0) - - def command_handler(self, command, *args, **kwargs): - """ - handle commands from the cc channel - """ - # add 'command_' suffix in order to executing command via bindctl - name = 'command_' + command - - if name in self.events: - event = self.events[name] - return event(*args, **kwargs) - else: - return self.command_unknown(command, args) - - def command_shutdown(self, args): - """ - handle shutdown command - """ - logger.info(STATS_RECEIVED_SHUTDOWN_COMMAND) - self.subject.running = False - return create_answer(0) - - def command_set(self, args, stats_data={}): - """ - handle set command - """ - # 'args' must be dictionary type - self.stats_data.update(args['stats_data']) - - # overwrite "stats.LastUpdateTime" - self.stats_data['stats.last_update_time'] = get_datetime() - - return create_answer(0) - - def command_remove(self, args, stats_item_name=''): - """ - handle remove command - """ - - # 'args' must be dictionary type - if args and args['stats_item_name'] in self.stats_data: - stats_item_name = args['stats_item_name'] - - logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_REMOVE_COMMAND, - stats_item_name) - - # just remove one item - self.stats_data.pop(stats_item_name) - - return create_answer(0) - - def command_show(self, args, stats_item_name=''): - """ - handle show command - """ - - # always overwrite 'report_time' and 'stats.timestamp' - # if "show" command invoked - self.stats_data['report_time'] = get_datetime() - self.stats_data['stats.timestamp'] = get_timestamp() - - # if with args - if args and args['stats_item_name'] in self.stats_data: - stats_item_name = args['stats_item_name'] - logger.debug(DBG_STATS_MESSAGING, - STATS_RECEIVED_SHOW_NAME_COMMAND, - stats_item_name) - return create_answer(0, {stats_item_name: self.stats_data[stats_item_name]}) - - logger.debug(DBG_STATS_MESSAGING, - STATS_RECEIVED_SHOW_ALL_COMMAND) - return create_answer(0, self.stats_data) - - def command_reset(self, args): - """ - handle reset command - """ - logger.debug(DBG_STATS_MESSAGING, - STATS_RECEIVED_RESET_COMMAND) - - # re-initialize internal variables - self.stats_data = self.initialize_data(self.stats_spec) - - # reset initial value - self.stats_data['stats.boot_time'] = self.boot_time - self.stats_data['stats.start_time'] = get_datetime() - self.stats_data['stats.last_update_time'] = get_datetime() - self.stats_data['stats.lname'] = self.session.lname - - return create_answer(0) - - def command_status(self, args): - """ - handle status command - """ - logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_STATUS_COMMAND) - # just return "I'm alive." - return create_answer(0, "I'm alive.") - - def command_unknown(self, command, args): - """ - handle an unknown command - """ - logger.error(STATS_RECEIVED_UNKNOWN_COMMAND, command) - return create_answer(1, "Unknown command: '"+str(command)+"'") - - - def initialize_data(self, spec): - """ - initialize stats data - """ - def __get_init_val(spec): - if spec['item_type'] == 'null': - return None - elif spec['item_type'] == 'boolean': - return bool(spec.get('item_default', False)) - elif spec['item_type'] == 'string': - return str(spec.get('item_default', '')) - elif spec['item_type'] in set(['number', 'integer']): - return int(spec.get('item_default', 0)) - elif spec['item_type'] in set(['float', 'double', 'real']): - return float(spec.get('item_default', 0.0)) - elif spec['item_type'] in set(['list', 'array']): - return spec.get('item_default', - [ __get_init_val(s) for s in spec['list_item_spec'] ]) - elif spec['item_type'] in set(['map', 'object']): - return spec.get('item_default', - dict([ (s['item_name'], __get_init_val(s)) for s in spec['map_item_spec'] ]) ) - else: - return spec.get('item_default') - return dict([ (s['item_name'], __get_init_val(s)) for s in spec ]) + SPECFILE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@" + os.sep + "stats.spec" + SPECFILE_LOCATION = SPECFILE_LOCATION.replace("${datarootdir}", DATAROOTDIR)\ + .replace("${prefix}", PREFIX) def get_timestamp(): """ @@ -388,33 +61,314 @@ def get_timestamp(): """ return time() -def get_datetime(): +def get_datetime(gmt=None): """ get current datetime """ - return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + if not gmt: gmt = gmtime() + return strftime("%Y-%m-%dT%H:%M:%SZ", gmt) -def main(session=None): +def parse_spec(spec): + """ + parse spec type data + """ + def _parse_spec(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", + [ _parse_spec(s) for s in spec["list_item_spec"] ]) + elif item_type == "map": + return spec.get( + "item_default", + dict([ (s["item_name"], _parse_spec(s)) for s in spec["map_item_spec"] ]) ) + else: + return spec.get("item_default", None) + return dict([ (s['item_name'], _parse_spec(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 = parse_spec(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 + # TODO: should be added into new logging interface + # if self.verbose: + # sys.stdout.write("[b10-stats] starting\n") + + # request Bob to send statistics data + logger.debug(DBG_STATS_MESSAGING, STATS_SEND_REQUEST_BOSS) + cmd = isc.config.ccsession.create_command("sendstats", None) + seq = self.cc_session.group_sendmsg(cmd, 'Boss') + self.cc_session.group_recvmsg(True, seq) + + # 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") + + while self.running: + self.mccs.check_command(False) + + 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): + """ + update information of each module + """ + 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, + "statistics" : [] } + if value[mod] and type(value[mod]) is list: + spec["statistics"] = value[mod] + modules[mod] = isc.config.module_spec.ModuleSpec(spec) + modules[self.module_name] = self.mccs.get_module_spec() + self.modules = modules + + def get_statistics_data(self, owner=None, name=None): + """ + return statistics data which stats module has of each module + """ + self.update_statistics_data() + if owner and name: + try: + return self.statistics_data[owner][name] + except KeyError: + pass + elif owner: + try: + return self.statistics_data[owner] + except KeyError: + pass + elif name: + pass + else: + return self.statistics_data + + def update_statistics_data(self, owner=None, **data): + """ + change statistics date of specified module into specified data + """ + self.update_modules() + statistics_data = {} + for (name, module) in self.modules.items(): + value = parse_spec(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') + 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) + if owner and not name: + return isc.config.create_answer(1, "item name is not specified") + errors = self.update_statistics_data( + self.module_name, + timestamp=get_timestamp(), + report_time=get_datetime() + ) + if errors: raise StatsError("stats spec file is incorrect") + ret = self.get_statistics_data(owner, name) + if ret: + return isc.config.create_answer(0, ret) + else: + return isc.config.create_answer( + 1, "specified module name and/or item name are incorrect") + + def command_showschema(self, owner=None, name=None): + """ + handle show command + """ + # TODO: should be added into new logging interface + # if self.verbose: + # sys.stdout.write("[b10-stats] 'showschema' command received\n") + 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, schema_byname[owner][name]) + else: + return isc.config.create_answer(0, 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 module name and/or item name are incorrect") + + def command_set(self, owner, data): + """ + handle set command + """ + errors = self.update_statistics_data(owner, **data) + if errors: + return isc.config.create_answer( + 1, + "specified module name and/or statistics data are 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") + 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") + 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) - subject = SessionSubject(session=session) - listener = CCSessionListener(subject) - subject.start() - while subject.running: - subject.check() - subject.stop() - + stats = Stats() + stats.start() except OptionValueError as ove: logger.fatal(STATS_BAD_OPTION_VALUE, ove) except SessionError as se: logger.fatal(STATS_CC_SESSION_ERROR, se) + # TODO: should be added into new logging interface + except StatsError as se: + sys.exit("[b10-stats] %s" % se) except KeyboardInterrupt as kie: logger.info(STATS_STOPPED_BY_KEYBOARD) - -if __name__ == "__main__": - main()