123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- #!@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 = {}
- # statistics data by each pid
- self.statistics_data_bypid = {}
- # 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, pid=-1, **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
- messages. If there is no error or if neither owner nor data is
- specified in args, it returns None. pid is the process id of
- the sender module in order for stats to identify which
- instance sends statistics data in the situation that multiple
- instances are working.
- """
- # Note:
- # The fix of #1751 is for multiple instances working. It is
- # assumed here that they send different statistics data with
- # each PID. Stats should save their statistics data by
- # PID. The statistics data, which is the existing variable, is
- # preserved by accumlating from statistics data by PID. This
- # is an ad-hoc fix because administrators can not see
- # statistics by each instance via bindctl or HTTP/XML. These
- # interfaces aren't changed in this fix.
- def _accum_bymodule(statistics_data_bypid):
- # This is an internal function for the superordinate
- # function. It accumulates statistics data of each PID by
- # module. It returns the accumulation result.
- def _accum(a, b):
- # If the first arg is dict or list type, two values
- # would be merged and accumlated.
- if type(a) is dict:
- return dict([ (k, _accum(v, b[k])) \
- if k in b else (k, v) \
- for (k, v) in a.items() ] \
- + [ (k, v) \
- for (k, v) in b.items() \
- if k not in a ])
- elif type(a) is list:
- return [ _accum(a[i], b[i]) \
- if len(b) > i else a[i] \
- for i in range(len(a)) ] \
- + [ b[i] \
- for i in range(len(b)) \
- if len(a) <= i ]
- # If the first arg is integer or float type, two
- # values are just added.
- elif type(a) is int or type(a) is float:
- return a + b
- # If the first arg is str or other types than above,
- # then it just returns the first arg which is assumed
- # to be the newer value.
- return a
- ret = {}
- for data in statistics_data_bypid.values():
- ret.update(_accum(data, ret))
- return ret
- # Firstly, it gets default statistics data in each spec file.
- 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
- self.statistics_data = statistics_data
- # If the "owner" and "data" arguments in this function are
- # specified, then the variable of statistics data of each pid
- # would be updated.
- errors = []
- if owner and data:
- try:
- if self.modules[owner].validate_statistics(False, data, errors):
- if owner in self.statistics_data_bypid:
- if pid in self.statistics_data_bypid[owner]:
- self.statistics_data_bypid[owner][pid].update(data)
- else:
- self.statistics_data_bypid[owner][pid] = data
- else:
- self.statistics_data_bypid[owner] = { pid : data }
- except KeyError:
- errors.append("unknown module name: " + str(owner))
- # If there are inactive instances, which was actually running
- # on the system before, their statistics data would be
- # removed. To find inactive instances, it invokes the
- # "show_processes" command to Boss via the cc session. Then it
- # gets active instance list and compares its PIDs with PIDs in
- # statistics data which it already has. If inactive instances
- # are found, it would remove their statistics data.
- seq = self.cc_session.group_sendmsg(
- isc.config.ccsession.create_command("show_processes", None),
- "Boss")
- (answer, env) = self.cc_session.group_recvmsg(False, seq)
- if answer:
- (rcode, value) = isc.config.ccsession.parse_answer(answer)
- if rcode == 0:
- if type(value) is list and len(value) > 0 \
- and type(value[0]) is list and len(value[0]) > 1:
- mlist = [ k for k in self.statistics_data_bypid.keys() ]
- for m in mlist:
- # PID list which it has before except for -1
- plist1 = [ p for p in self.statistics_data_bypid[m]\
- .keys() if p != -1]
- # PID list of active instances which is
- # received from Boss
- plist2 = [ v[0] for v in value \
- if v[1].lower().find(m.lower()) \
- >= 0 ]
- # get inactive instance list by the difference
- # between plist1 and plist2
- nplist = set(plist1).difference(set(plist2))
- for p in nplist:
- self.statistics_data_bypid[m].pop(p)
- if self.statistics_data_bypid[m]:
- if m in self.statistics_data:
- self.statistics_data[m].update(
- _accum_bymodule(
- self.statistics_data_bypid[m]))
- # remove statistics data of the module with no
- # PID
- else:
- self.statistics_data_bypid.pop(m)
- if errors: 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, pid=None):
- """
- handle shutdown command
- The pid argument is ignored, it is here to match the signature.
- """
- 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, pid=-1, data={}):
- """
- handle set command
- """
- errors = self.update_statistics_data(owner, pid, **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)
|