| 
 #!/usr/bin/python3  
  
# 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 = "/home/jelte/opt/bind10"  
    DATAROOTDIR = "${prefix}/share"  
    SPECFILE_LOCATION = "${datarootdir}" + os.sep + "bind10-devel" + 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)  
180            if answer:  
                rcode, args = isc.config.ccsession.parse_answer(answer)  
180                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())  
174                    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)  
            )  
186        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)  
242        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)  
392        if answer:  
            (rcode, value) = isc.config.ccsession.parse_answer(answer)  
392            if rcode == 0:  
392   392                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)  
  
494if __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)  
                
             |