diff --git a/ChangeLog b/ChangeLog index f50c99b0f3..62f6721a44 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,10 @@ + 109. [func] naokikambe + Added the initial version of the stats module for the statistics + feature of BIND 10, which supports the restricted features and + items and reports via bindctl command (Trac #191, rXXXX) + Added the document of the stats module, which is about how stats + module collects the data (Trac #170, [wiki:StatsModule]) + 108. [func] jerry src/bin/zonemgr: Provide customizable configurations for lowerbound_refresh, lowerbound_retry, max_transfer_timeout and diff --git a/configure.ac b/configure.ac index 6bf51c5195..b7709f13f0 100644 --- a/configure.ac +++ b/configure.ac @@ -471,6 +471,8 @@ AC_CONFIG_FILES([Makefile src/bin/xfrout/tests/Makefile src/bin/zonemgr/Makefile src/bin/zonemgr/tests/Makefile + src/bin/stats/Makefile + src/bin/stats/tests/Makefile src/bin/usermgr/Makefile src/bin/tests/Makefile src/lib/Makefile @@ -523,6 +525,12 @@ AC_OUTPUT([src/bin/cfgmgr/b10-cfgmgr.py src/bin/zonemgr/zonemgr.spec.pre src/bin/zonemgr/tests/zonemgr_test src/bin/zonemgr/run_b10-zonemgr.sh + src/bin/stats/stats.py + src/bin/stats/stats_stub.py + src/bin/stats/stats.spec.pre + src/bin/stats/run_b10-stats.sh + src/bin/stats/run_b10-stats_stub.sh + src/bin/stats/tests/stats_test src/bin/bind10/bind10.py src/bin/bind10/tests/bind10_test src/bin/bind10/run_bind10.sh @@ -556,6 +564,9 @@ AC_OUTPUT([src/bin/cfgmgr/b10-cfgmgr.py chmod +x src/bin/xfrin/run_b10-xfrin.sh chmod +x src/bin/xfrout/run_b10-xfrout.sh chmod +x src/bin/zonemgr/run_b10-zonemgr.sh + chmod +x src/bin/stats/tests/stats_test + chmod +x src/bin/stats/run_b10-stats.sh + chmod +x src/bin/stats/run_b10-stats_stub.sh chmod +x src/bin/bind10/run_bind10.sh chmod +x src/bin/cmdctl/tests/cmdctl_test chmod +x src/bin/xfrin/tests/xfrin_test diff --git a/src/bin/Makefile.am b/src/bin/Makefile.am index 1a61ca78be..65ca0e6356 100644 --- a/src/bin/Makefile.am +++ b/src/bin/Makefile.am @@ -1,4 +1,4 @@ SUBDIRS = bind10 bindctl cfgmgr loadzone msgq host cmdctl auth xfrin xfrout \ - usermgr zonemgr tests + usermgr zonemgr stats tests check-recursive: all-recursive diff --git a/src/bin/bind10/bind10.py.in b/src/bin/bind10/bind10.py.in index 0cee21e42a..c8ae559e0a 100644 --- a/src/bin/bind10/bind10.py.in +++ b/src/bin/bind10/bind10.py.in @@ -73,6 +73,9 @@ isc.utils.process.rename(sys.argv[0]) # number, and the overall BIND 10 version number (set in configure.ac). VERSION = "bind10 20100916 (BIND 10 @PACKAGE_VERSION@)" +# This is for bind10.boottime of stats module +_BASETIME = time.gmtime() + class RestartSchedule: """ Keeps state when restarting something (in this case, a process). @@ -424,6 +427,27 @@ class BoB: sys.stdout.write("[bind10] Started b10-zonemgr(PID %d)\n" % zonemgr.pid) + # start b10-stats + stats_args = ['b10-stats'] + if self.verbose: + sys.stdout.write("[bind10] Starting b10-stats\n") + stats_args += ['-v'] + try: + statsd = ProcessInfo("b10-stats", stats_args, + c_channel_env) + except Exception as e: + c_channel.process.kill() + bind_cfgd.process.kill() + xfrout.process.kill() + auth.process.kill() + xfrind.process.kill() + zonemgr.process.kill() + return "Unable to start b10-stats; " + str(e) + + self.processes[statsd.pid] = statsd + if self.verbose: + sys.stdout.write("[bind10] Started b10-stats (PID %d)\n" % statsd.pid) + # start the b10-cmdctl # XXX: we hardcode port 8080 cmdctl_args = ['b10-cmdctl'] @@ -440,6 +464,7 @@ class BoB: auth.process.kill() xfrind.process.kill() zonemgr.process.kill() + statsd.process.kill() return "Unable to start b10-cmdctl; " + str(e) self.processes[cmd_ctrld.pid] = cmd_ctrld if self.verbose: @@ -459,6 +484,7 @@ class BoB: self.cc_session.group_sendmsg(cmd, "Boss", "Xfrout") self.cc_session.group_sendmsg(cmd, "Boss", "Xfrin") self.cc_session.group_sendmsg(cmd, "Boss", "Zonemgr") + self.cc_session.group_sendmsg(cmd, "Boss", "Stats") def stop_process(self, process): """Stop the given process, friendly-like.""" @@ -723,6 +749,17 @@ def main(): sys.exit(1) sys.stdout.write("[bind10] BIND 10 started\n") + # send "bind10.boot_time" to b10-stats + time.sleep(1) # wait a second + if options.verbose: + sys.stdout.write("[bind10] send \"bind10.boot_time\" to b10-stats\n") + cmd = isc.config.ccsession.create_command('set', + { "stats_data": { + 'bind10.boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', _BASETIME) + } + }) + boss_of_bind.cc_session.group_sendmsg(cmd, 'Stats') + # In our main loop, we check for dead processes or messages # on the c-channel. wakeup_fd = wakeup_pipe[0] diff --git a/src/bin/bind10/run_bind10.sh.in b/src/bin/bind10/run_bind10.sh.in index defb630d50..18c29645bf 100644 --- a/src/bin/bind10/run_bind10.sh.in +++ b/src/bin/bind10/run_bind10.sh.in @@ -20,7 +20,7 @@ export PYTHON_EXEC BIND10_PATH=@abs_top_builddir@/src/bin/bind10 -PATH=@abs_top_builddir@/src/bin/msgq:@abs_top_builddir@/src/bin/auth:@abs_top_builddir@/src/bin/cfgmgr:@abs_top_builddir@/src/bin/cmdctl:@abs_top_builddir@/src/bin/xfrin:@abs_top_builddir@/src/bin/xfrout:@abs_top_builddir@/src/bin/zonemgr:$PATH +PATH=@abs_top_builddir@/src/bin/msgq:@abs_top_builddir@/src/bin/auth:@abs_top_builddir@/src/bin/cfgmgr:@abs_top_builddir@/src/bin/cmdctl:@abs_top_builddir@/src/bin/stats:@abs_top_builddir@/src/bin/xfrin:@abs_top_builddir@/src/bin/xfrout:@abs_top_builddir@/src/bin/zonemgr:$PATH export PATH PYTHONPATH=@abs_top_builddir@/src/lib/python:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/xfr/.libs diff --git a/src/bin/stats/Makefile.am b/src/bin/stats/Makefile.am new file mode 100644 index 0000000000..b267479de6 --- /dev/null +++ b/src/bin/stats/Makefile.am @@ -0,0 +1,37 @@ +SUBDIRS = tests + +pkglibexecdir = $(libexecdir)/@PACKAGE@ + +pkglibexec_SCRIPTS = b10-stats +noinst_SCRIPTS = b10-stats_stub + +b10_statsdir = $(DESTDIR)$(pkgdatadir) +b10_stats_DATA = stats.spec + +CLEANFILES = stats.spec b10-stats stats.pyc stats.pyo b10-stats_stub stats_stub.pyc stats_stub.pyo + +man_MANS = b10-stats.8 +EXTRA_DIST = $(man_MANS) b10-stats.xml + +if ENABLE_MAN + +b10-stats.8: b10-stats.xml + xsltproc --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-stats.xml + +endif + +stats.spec: stats.spec.pre + $(SED) -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" stats.spec.pre >$@ + +# TODO: does this need $$(DESTDIR) also? +# this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix +b10-stats: stats.py + $(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" \ + -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" \ + -e "s|.*#@@REMOVED@@$$||" stats.py >$@ + chmod a+x $@ + +b10-stats_stub: stats_stub.py stats.py + $(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" \ + -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" stats_stub.py >$@ + chmod a+x $@ diff --git a/src/bin/stats/b10-stats.8 b/src/bin/stats/b10-stats.8 new file mode 100644 index 0000000000..062ff35d02 --- /dev/null +++ b/src/bin/stats/b10-stats.8 @@ -0,0 +1,68 @@ +'\" t +.\" Title: b10-stats +.\" Author: [FIXME: author] [see http://docbook.sf.net/el/author] +.\" Generator: DocBook XSL Stylesheets v1.75.2 +.\" Date: Oct 15, 2010 +.\" Manual: BIND10 +.\" Source: BIND10 +.\" Language: English +.\" +.TH "B10\-STATS" "8" "Oct 15, 2010" "BIND10" "BIND10" +.\" ----------------------------------------------------------------- +.\" * set default formatting +.\" ----------------------------------------------------------------- +.\" disable hyphenation +.nh +.\" disable justification (adjust text to left margin only) +.ad l +.\" ----------------------------------------------------------------- +.\" * MAIN CONTENT STARTS HERE * +.\" ----------------------------------------------------------------- +.SH "NAME" +b10-stats \- BIND 10 statistics module +.SH "SYNOPSIS" +.HP \w'\fBb10\-stats\fR\ 'u +\fBb10\-stats\fR [\fB\-v\fR] [\fB\-\-verbose\fR] +.SH "DESCRIPTION" +.PP +The +\fBb10\-stats\fR +is a daemon forked by +\fBbind10\fR\&. Stats module collects statistics data from each module and reports statistics information via +\fBbindctl\fR\&. It communicates by using the Command Channel by +\fBb10\-msgq\fR +with other modules like +\fBbind10\fR, +\fBb10\-auth\fR +and so on\&. It waits for coming data from other modules, then other modules send data to stats module periodically\&. Other modules send stats data to stats module independently from implementation of stats module, so the frequency of sending data may not be constant\&. Stats module collects data and aggregates it\&. +.SH "OPTIONS" +.PP +The arguments are as follows: +.PP +\fB\-v\fR, \fB\-\-verbose\fR +.RS 4 +This +\fBb10\-stats\fR +switches to verbose mode\&. It sends verbose messages to STDOUT\&. +.RE +.SH "FILES" +.PP +/usr/local/share/bind10\-devel/stats\&.spec +\(em This is a spec file for +\fBb10\-stats\fR\&. It contains definitions of statistics items of BIND 10 and commands received vi bindctl\&. +.SH "SEE ALSO" +.PP + +\fBbind10\fR(8), +\fBbindctl\fR(1), +\fBb10-auth\fR(8), +BIND 10 Guide\&. +.SH "HISTORY" +.PP +The +\fBb10\-stats\fR +daemon was initially designed and implemented by Naoki Kambe of JPRS in Oct 2010\&. +.SH "COPYRIGHT" +.br +Copyright \(co 2010 Internet Systems Consortium, Inc. ("ISC") +.br diff --git a/src/bin/stats/b10-stats.xml b/src/bin/stats/b10-stats.xml new file mode 100644 index 0000000000..fb82ecd807 --- /dev/null +++ b/src/bin/stats/b10-stats.xml @@ -0,0 +1,124 @@ +]> + + + + + + + Oct 15, 2010 + + + + b10-stats + 8 + BIND10 + + + + b10-stats + BIND 10 statistics module + + + + + 2010 + Internet Systems Consortium, Inc. ("ISC") + + + + + + b10-stats + + + + + + + DESCRIPTION + + The b10-stats is a daemon forked by + bind10. Stats module collects statistics data + from each module and reports statistics information + via bindctl. It communicates by using the + Command Channel by b10-msgq with other + modules + like bind10, b10-auth and + so on. It waits for coming data from other modules, then other + modules send data to stats module periodically. Other modules + send stats data to stats module independently from + implementation of stats module, so the frequency of sending data + may not be constant. Stats module collects data and aggregates + it. + + + + + OPTIONS + The arguments are as follows: + + + , + + + This b10-stats switches to verbose + mode. It sends verbose messages to STDOUT. + + + + + + + + FILES + /usr/local/share/bind10-devel/stats.spec + — This is a spec file for b10-stats. It + contains definitions of statistics items of BIND 10 and commands + received vi bindctl. + + + + + SEE ALSO + + + bind108 + , + + bindctl1 + , + + b10-auth8 + , + BIND 10 Guide. + + + + + HISTORY + + The b10-stats daemon was initially designed + and implemented by Naoki Kambe of JPRS in Oct 2010. + + + diff --git a/src/bin/stats/run_b10-stats.sh.in b/src/bin/stats/run_b10-stats.sh.in new file mode 100644 index 0000000000..c23df285bb --- /dev/null +++ b/src/bin/stats/run_b10-stats.sh.in @@ -0,0 +1,30 @@ +#! /bin/sh + +# Copyright (C) 2010 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. + +PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@} +export PYTHON_EXEC + +PYTHONPATH=@abs_top_builddir@/src/lib/python +export PYTHONPATH + +B10_FROM_BUILD=@abs_top_builddir@ +export B10_FROM_BUILD + +STATS_PATH=@abs_top_builddir@/src/bin/stats + +cd ${STATS_PATH} +exec ${PYTHON_EXEC} -O b10-stats $* diff --git a/src/bin/stats/run_b10-stats_stub.sh.in b/src/bin/stats/run_b10-stats_stub.sh.in new file mode 100644 index 0000000000..03c4584cc2 --- /dev/null +++ b/src/bin/stats/run_b10-stats_stub.sh.in @@ -0,0 +1,30 @@ +#! /bin/sh + +# Copyright (C) 2010 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. + +PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@} +export PYTHON_EXEC + +PYTHONPATH=@abs_top_builddir@/src/lib/python +export PYTHONPATH + +B10_FROM_BUILD=@abs_top_srcdir@ +export B10_FROM_BUILD + +STATS_PATH=@abs_top_builddir@/src/bin/stats + +cd ${STATS_PATH} +exec ${PYTHON_EXEC} -O b10-stats_stub $* diff --git a/src/bin/stats/stats.py.in b/src/bin/stats/stats.py.in new file mode 100644 index 0000000000..eb44d5980b --- /dev/null +++ b/src/bin/stats/stats.py.in @@ -0,0 +1,416 @@ +#!@PYTHON@ + +# Copyright (C) 2010 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. + +# $Id$ +__version__ = "$Revision$" + +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 +# Note: Following lines are removed in b10-stats #@@REMOVED@@ +if __name__ == 'stats': #@@REMOVED@@ + try: #@@REMOVED@@ + from fake_time import time, strftime, gmtime #@@REMOVED@@ + except ImportError: #@@REMOVED@@ + pass #@@REMOVED@@ + +# for setproctitle +import isc.utils.process +isc.utils.process.rename() + +# If B10_FROM_BUILD 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_BUILD" in os.environ: + SPECFILE_LOCATION = os.environ["B10_FROM_BUILD"] + "/src/bin/stats/stats.spec" +else: + PREFIX = "@prefix@" + DATAROOTDIR = "@datarootdir@" + SPECFILE_LOCATION = "@datadir@/@PACKAGE@/stats.spec".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX) + +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, verbose=False): + Subject.__init__(self) + self.verbose = verbose + 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, verbose=False): + Listener.__init__(self, subject) + self.verbose = verbose + 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.config_spec = self.cc_session.get_module_spec().get_config_spec() + self.stats_spec = self.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: + sys.stderr.write("[b10-stats] Caught undefined command while parsing spec file: " + +str(cmd["command_name"])+"\n") + + 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 + return self.cc_session.start() + + 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() + + def config_handler(self, new_config): + """ + handle a configure from the cc channel + """ + if self.verbose: + sys.stdout.write("[b10-stats] newconfig received: "+str(new_config)+"\n") + + # 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 + """ + if self.verbose: + sys.stdout.write("[b10-stats] 'shutdown' command received\n") + self.subject.running = False + return create_answer(0) + + def command_set(self, args, stats_data={}): + """ + handle set command + """ + if self.verbose: + sys.stdout.write("[b10-stats] 'set' command received, args: "+str(args)+"\n") + + # '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 + """ + if self.verbose: + sys.stdout.write("[b10-stats] 'remove' command received, args: "+str(args)+"\n") + + # 'args' must be dictionary type + if args and args['stats_item_name'] in self.stats_data: + stats_item_name = args['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 + """ + if self.verbose: + sys.stdout.write("[b10-stats] 'show' command received, args: "+str(args)+"\n") + + # 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'] + return create_answer(0, {stats_item_name: self.stats_data[stats_item_name]}) + + return create_answer(0, self.stats_data) + + def command_reset(self, args): + """ + handle reset command + """ + if self.verbose: + sys.stdout.write("[b10-stats] 'reset' command received\n") + + # 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 + """ + if self.verbose: + sys.stdout.write("[b10-stats] 'status' command received\n") + # just return "I'm alive." + return create_answer(0, "I'm alive.") + + def command_unknown(self, command, args): + """ + handle an unknown command + """ + if self.verbose: + sys.stdout.write("[b10-stats] Unknown command received: '" + + str(command) + "'\n") + 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 ]) + +def get_timestamp(): + """ + get current timestamp + """ + return time() + +def get_datetime(): + """ + get current datetime + """ + return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + +def main(session=None): + 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() + subject = SessionSubject(session=session, verbose=options.verbose) + listener = CCSessionListener(subject, verbose=options.verbose) + subject.start() + while subject.running: + subject.check() + subject.stop() + + except OptionValueError: + sys.stderr.write("[b10-stats] Error parsing options\n") + except SessionError as se: + sys.stderr.write("[b10-stats] Error creating Stats module, " + + "is the command channel daemon running?\n") + except KeyboardInterrupt as kie: + sys.stderr.write("[b10-stats] Interrupted, exiting\n") + +if __name__ == "__main__": + main() diff --git a/src/bin/stats/stats.spec.pre.in b/src/bin/stats/stats.spec.pre.in new file mode 100644 index 0000000000..6970250d98 --- /dev/null +++ b/src/bin/stats/stats.spec.pre.in @@ -0,0 +1,140 @@ +{ + "module_spec": { + "module_name": "Stats", + "module_description": "Stats daemon", + "config_data": [ + { + "item_name": "report_time", + "item_type": "string", + "item_optional": false, + "item_default": "1970-01-01T00:00:00Z", + "item_title": "Report time", + "item_description": "A date time when stats module reports", + "item_format": "date-time" + }, + { + "item_name": "bind10.boot_time", + "item_type": "string", + "item_optional": false, + "item_default": "1970-01-01T00:00:00Z", + "item_title": "stats.BootTime", + "item_description": "A date time when bind10 process starts initially", + "item_format": "date-time" + }, + { + "item_name": "stats.boot_time", + "item_type": "string", + "item_optional": false, + "item_default": "1970-01-01T00:00:00Z", + "item_title": "stats.BootTime", + "item_description": "A date time when the stats module starts initially or when the stats module restarts", + "item_format": "date-time" + }, + { + "item_name": "stats.start_time", + "item_type": "string", + "item_optional": false, + "item_default": "1970-01-01T00:00:00Z", + "item_title": "stats.StartTime", + "item_description": "A date time when the stats module starts collecting data or resetting values last time", + "item_format": "date-time" + }, + { + "item_name": "stats.last_update_time", + "item_type": "string", + "item_optional": false, + "item_default": "1970-01-01T00:00:00Z", + "item_title": "stats.LastUpdateTime", + "item_description": "The latest date time when the stats module receives from other modules like auth server or boss process and so on", + "item_format": "date-time" + }, + { + "item_name": "stats.timestamp", + "item_type": "real", + "item_optional": false, + "item_default": 0.0, + "item_title": "stats.Timestamp", + "item_description": "A current time stamp since epoch time (1970-01-01T00:00:00Z)", + "item_format": "second" + }, + { + "item_name": "stats.lname", + "item_type": "string", + "item_optional": false, + "item_default": "", + "item_title": "stats.LocalName", + "item_description": "A localname of stats module given via CC protocol" + }, + { + "item_name": "auth.queries.tcp", + "item_type": "integer", + "item_optional": false, + "item_default": 0, + "item_title": "auth.queries.tcp", + "item_description": "A number of total query counts which all auth servers receive over TCP since they started initially" + }, + { + "item_name": "auth.queries.udp", + "item_type": "integer", + "item_optional": false, + "item_default": 0, + "item_title": "auth.queries.udp", + "item_description": "A number of total query counts which all auth servers receive over UDP since they started initially" + } + ], + "commands": [ + { + "command_name": "status", + "command_description": "identify whether stats module is alive or not", + "command_args": [] + }, + { + "command_name": "show", + "command_description": "show the specified/all statistics data", + "command_args": [ + { + "item_name": "stats_item_name", + "item_type": "string", + "item_optional": true, + "item_default": "" + } + ] + }, + { + "command_name": "set", + "command_description": "set the value of specified name in statistics data", + "command_args": [ + { + "item_name": "stats_data", + "item_type": "map", + "item_optional": false, + "item_default": {}, + "map_item_spec": [] + } + ] + }, + { + "command_name": "remove", + "command_description": "remove the specified name from statistics data", + "command_args": [ + { + "item_name": "stats_item_name", + "item_type": "string", + "item_optional": false, + "item_default": "" + } + ] + }, + { + "command_name": "reset", + "command_description": "reset all statistics data to default values except for several constant names", + "command_args": [] + }, + { + "command_name": "shutdown", + "command_description": "Shut down the stats module", + "command_args": [] + } + ] + } +} diff --git a/src/bin/stats/stats_stub.py.in b/src/bin/stats/stats_stub.py.in new file mode 100644 index 0000000000..4efa73808d --- /dev/null +++ b/src/bin/stats/stats_stub.py.in @@ -0,0 +1,155 @@ +#!@PYTHON@ + +# Copyright (C) 2010 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. + +# $Id$ +__version__ = "$Revision$" + +import sys; sys.path.append ('@@PYTHONPATH@@') +import os +import time +from optparse import OptionParser, OptionValueError +from isc.config.ccsession import ModuleCCSession, create_command, parse_answer, parse_command, create_answer +from isc.cc import Session, SessionError +from stats import get_datetime + +# for setproctitle +import isc.utils.process +isc.utils.process.rename() + +# If B10_FROM_BUILD 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_BUILD" in os.environ: + SPECFILE_LOCATION = os.environ["B10_FROM_BUILD"] + "/src/bin/stats/stats.spec" +else: + PREFIX = "@prefix@" + DATAROOTDIR = "@datarootdir@" + SPECFILE_LOCATION = "@datadir@/@PACKAGE@/stats.spec".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX) + +class CCSessionStub: + """ + This class is intended to behaves as a sender to Stats module. It + creates MoudleCCSession object and send specified command. + """ + def __init__(self, session=None, verbose=False): + # create ModuleCCSession object + self.verbose = verbose + self.cc_session = ModuleCCSession(SPECFILE_LOCATION, + self.__dummy, self.__dummy, session) + self.module_name = self.cc_session._module_name + self.session = self.cc_session._session + + def __dummy(self, *args): + pass + + def send_command(self, command, args): + """ + send command to stats module with args + """ + cmd = create_command(command, args) + if self.verbose: + sys.stdout.write("[b10-stats_stub] send command : " + str(cmd) + "\n") + seq = self.session.group_sendmsg(cmd, self.module_name) + msg, env = self.session.group_recvmsg(False, seq) # non-blocking is False + if self.verbose: + sys.stdout.write("[b10-stats_stub] received env : " + str(env) + "\n") + sys.stdout.write("[b10-stats_stub] received message : " + str(msg) + "\n") + (ret, arg) = (None, None) + if 'result' in msg: + ret, arg = parse_answer(msg) + elif 'command' in msg: + ret, arg = parse_command(msg) + self.session.group_reply(env, create_answer(0)) + return ret, arg, env + +class BossModuleStub: + """ + This class is customized from CCSessionStub and is intended to behaves + as a virtual Boss module to send to Stats Module. + """ + def __init__(self, session=None, verbose=False): + self.stub = CCSessionStub(session=session, verbose=verbose) + + def send_boottime(self): + return self.stub.send_command("set", {"stats_data": {"bind10.boot_time": get_datetime()}}) + +class AuthModuleStub: + """ + This class is customized CCSessionStub and is intended to behaves + as a virtual Auth module to send to Stats Module. + """ + def __init__(self, session=None, verbose=False): + self.stub = CCSessionStub(session=session, verbose=verbose) + self.count = { "udp": 0, "tcp": 0 } + + def send_udp_query_count(self, cmd="set", cnt=0): + """ + count up udp query count + """ + prt = "udp" + self.count[prt] = 1 + if cnt > 0: + self.count[prt] = cnt + return self.stub.send_command(cmd, + {"stats_data": + {"auth.queries."+prt: self.count[prt]} + }) + + def send_tcp_query_count(self, cmd="set", cnt=0): + """ + set udp query count + """ + prt = "tcp" + self.count[prt] = self.count[prt] + 1 + if cnt > 0: + self.count[prt] = cnt + return self.stub.send_command(cmd, + {"stats_data": + {"auth.queries."+prt: self.count[prt]} + }) + +def main(session=None): + 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() + stub = CCSessionStub(session=session, verbose=options.verbose) + boss = BossModuleStub(session=stub.session, verbose=options.verbose) + auth = AuthModuleStub(session=stub.session, verbose=options.verbose) + stub.send_command("status", None) + boss.send_boottime() + t_cnt=0 + u_cnt=81120 + auth.send_udp_query_count(cnt=u_cnt) # This is an example. + while True: + u_cnt = u_cnt + 1 + t_cnt = t_cnt + 1 + auth.send_udp_query_count(cnt=u_cnt) + auth.send_tcp_query_count(cnt=t_cnt) + time.sleep(1) + + except OptionValueError: + sys.stderr.write("[b10-stats_stub] Error parsing options\n") + except SessionError as se: + sys.stderr.write("[b10-stats_stub] Error creating Stats module, " + + "is the command channel daemon running?\n") + except KeyboardInterrupt as kie: + sys.stderr.write("[b10-stats_stub] Interrupted, exiting\n") + +if __name__ == "__main__": + main() diff --git a/src/bin/stats/statsd.py b/src/bin/stats/statsd.py deleted file mode 100644 index 1e20c121a8..0000000000 --- a/src/bin/stats/statsd.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/python -# -# This program collects 'counters' from 'statistics' channel. -# It accepts one command: 'Boss' group 'shutdown' - -import isc.cc -import time -import select -import os - -bossgroup = 'Boss' -myname = 'statsd' -debug = 0 - -def total(s): - def totalsub(d,s): - for k in s.keys(): - if (k == 'component' or k == 'version' - or k == 'timestamp' or k == 'from'): - continue - if (k in d): - if (isinstance(s[k], dict)): - totalsub(d[k], s[k]) - else: - d[k] = s[k] + d[k] - else: - d[k] = s[k] - - if (len(s) == 0): - return {} - if (len(s) == 1): - for k in s.keys(): - out = s[k] - out['components'] = 1 - out['timestamp2'] = out['timestamp'] - del out['from'] - return out - _time1 = 0 - _time2 = 0 - out = {} - for i in s.values(): - if (_time1 == 0 or _time1 < i['timestamp']): - _time1 = i['timestamp'] - if (_time2 == 0 or _time2 > i['timestamp']): - _time2 = i['timestamp'] - totalsub(out, i) - out['components'] = len(s) - out['timestamp'] = _time1; - out['timestamp2'] = _time2; - return out - -def dicttoxml(stats, level = 0): - def dicttoxmlsub(s, level): - output = '' - spaces = ' ' * level - for k in s.keys(): - if (isinstance(s[k], dict)): - output += spaces + ('<%s>\n' %k) \ - + dicttoxmlsub(s[k], level+1) \ - + spaces + '\n' %k - else: - output += spaces + '<%s>%s\n' % (k, s[k], k) - return output - - for k in stats.keys(): - space = ' ' * level - output = space + '\n' % k - s = stats[k] - if ('component' in s or 'components' in s): - output += dicttoxmlsub(s, level+1) - else: - for l in s.keys(): - output += space + ' \n' % l \ - + dicttoxmlsub(s[l], level+2) \ - + space + ' \n' - output += space + '\n' - return output - -def dump_stats(statpath, statcount, stat, statraw): - newfile = open(statpath + '.new', 'w') - newfile.write('\n') - newfile.write('\n') - newfile.write('\n') - newfile.write(' \n') - newfile.write(' \n') - newfile.write(dicttoxml(stat, 3)) - newfile.write(' \n') - newfile.write(' \n') - newfile.write(dicttoxml(statraw, 3)) - newfile.write(' \n') - newfile.write(' \n') - newfile.write('\n') - newfile.close() - loop = statcount - while(loop > 0): - old = statpath + '.%d' % loop - loop -= 1 - new = statpath + '.%d' % loop - if (os.access(new, os.F_OK)): - os.rename(new, old) - if (os.access(statpath, os.F_OK)): - os.rename(statpath, new) - os.rename(statpath + '.new', statpath) - -def collector(statgroup,step,statpath,statcount): - cc = isc.cc.Session() - if debug: - print ("cc.lname=",cc.lname) - cc.group_subscribe(statgroup) - cc.group_subscribe(bossgroup, myname) - wrote_time = -1 - last_wrote_time = -1 - last_recvd_time = -1 - stats = {} - statstotal = {} - while 1: - wait = wrote_time + step - time.time() - if wait <= 0 and last_recvd_time > wrote_time: - if debug: - print ("dump stats") - dump_stats(statpath, statcount, statstotal, stats) - last_wrote_time = wrote_time; - wrote_time = time.time(); - wait = last_wrote_time + step - time.time() - if wait < 0: - wait = step - r,w,e = select.select([cc._socket],[],[], wait) - for sock in r: - if sock == cc._socket: - data,envelope = cc.group_recvmsg(False) - if (envelope['group'] == bossgroup): - if ('shutdown' in data): - exit() - if (envelope['group'] == statgroup): - # Check received data - if (not('component' in data and 'version' in data - and 'stats' in data)): - continue - component = data['component'] - _from = envelope['from'] - data['from'] = _from - if debug: - print ("received from ",_from) - if (not (component in stats)): - stats[component] = {} - (stats[component])[_from] = data; - statstotal[component] = total(stats[component]) - last_recvd_time = time.time() - -if __name__ == '__main__': - collector('statistics', 10, '/tmp/stats.xml', 100) diff --git a/src/bin/stats/statsd.txt b/src/bin/stats/statsd.txt deleted file mode 100755 index 6efd1741ac..0000000000 --- a/src/bin/stats/statsd.txt +++ /dev/null @@ -1,55 +0,0 @@ -= Statistics overview = - -Result of 26 Jan 2010 evening discussion, -statistics overview was almost fixed. - -Statsd listens msgq "statistics" channel, gathers statistics -from each BIND 10 components and dump them into a XML file periodically. - -= Statsd current status = - -Statsd can run with msgq. -Statsd is not controlled by BoB. -Statsd does not read configuration from cfgd. -File path, dump frequency, rotate generations are fixed. -Statsd dumps to "/tmp/stats" every 10 seconds except no statistics received. -"/tmp/stats" are preserved 100 generations. - -Current implementation is put on "bind10/branches/parkinglot/src/bin/stats/". - -= statistics channel Message format = - -The Statsd accepts python dictionary format data from msgq -"statistics" channel. - -The data need to contain "components", "version", "timestamp", "stats" keys. - -The statistics data format is { "component" : "", -"version": "", "timestamp": "", "stats": -}. - -"stats" data may be nested. -"stats" data is defined by each component. - -Each component sends statistics data to "statistics" group periodically -without joining the group. - -See a example component: "stats/test/test-agent.py". - -= How to publish statistics from each component = - -For example, parkinglot auth server has one "counter". -Then, parkinglot's statistics message may be - { "component":"parkinglot", "version":1, "timestamp":unixtime, - stats: { "counter": counter } }. -Send it to msgq "statistics" channel periodically -(For example, every 10 second). - -Then "Statsd" will write it to the statistics file periodically. - -= TODO = - -- statsd.spec -- read configuration from cfgd. -- how to publish statistics data -- controlled by BoB diff --git a/src/bin/stats/test/shutdown.py b/src/bin/stats/test/shutdown.py deleted file mode 100644 index e955226458..0000000000 --- a/src/bin/stats/test/shutdown.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/python - -import isc -cc = isc.cc.Session() -cc.group_subscribe("Boss") -cc.group_sendmsg({ "command":"shutdown"},"Boss") diff --git a/src/bin/stats/test/test_agent.py b/src/bin/stats/test/test_agent.py deleted file mode 100644 index 2a257adc43..0000000000 --- a/src/bin/stats/test/test_agent.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/python - -# This program acts statistics agent. -# It has pseudo counters which is incremented each 10 second and -# sends data to "statistics" channel periodically. -# One command is available -# "Boss" group: "shutdown" - -import isc -import time -import select -import random - -step_time = 10 -statgroup = "statistics" - -cc = isc.cc.Session() -print (cc.lname) -#cc.group_subscribe(statgroup) -cc.group_subscribe("Boss") - -# counters - -NSSTATDESC={} -NSSTATDESC["counterid"] = 0 -NSSTATDESC["requestv4"] = 0 -NSSTATDESC["requestv6"] = 0 -NSSTATDESC["edns0in"] = 0 -NSSTATDESC["badednsver"] = 0 -NSSTATDESC["tsigin"] = 0 -NSSTATDESC["sig0in"] = 0 -NSSTATDESC["invalidsig"] = 0 -NSSTATDESC["tcp"] = 0 -NSSTATDESC["authrej"] = 0 -NSSTATDESC["recurserej"] = 0 -NSSTATDESC["xfrrej"] = 0 -NSSTATDESC["updaterej"] = 0 -NSSTATDESC["response"] = 0 -NSSTATDESC["truncatedresp"] = 0 -NSSTATDESC["edns0out"] = 0 -NSSTATDESC["tsigout"] = 0 -NSSTATDESC["sig0out"] = 0 -NSSTATDESC["success"] = 0 -NSSTATDESC["authans"] = 0 -NSSTATDESC["nonauthans"] = 0 -NSSTATDESC["referral"] = 0 -NSSTATDESC["nxrrset"] = 0 -NSSTATDESC["servfail"] = 0 -NSSTATDESC["formerr"] = 0 -NSSTATDESC["nxdomain"] = 0 -NSSTATDESC["recursion"] = 0 -NSSTATDESC["duplicate"] = 0 -NSSTATDESC["dropped"] = 0 -NSSTATDESC["failure"] = 0 -NSSTATDESC["xfrdone"] = 0 -NSSTATDESC["updatereqfwd"] = 0 -NSSTATDESC["updaterespfwd"] = 0 -NSSTATDESC["updatefwdfail"] = 0 -NSSTATDESC["updatedone"] = 0 -NSSTATDESC["updatefail"] = 0 -NSSTATDESC["updatebadprereq"] = 0 -RESSTATDESC={} -RESSTATDESC["counterid"] = 0 -RESSTATDESC["queryv4"] = 0 -RESSTATDESC["queryv6"] = 0 -RESSTATDESC["responsev4"] = 0 -RESSTATDESC["responsev6"] = 0 -RESSTATDESC["nxdomain"] = 0 -RESSTATDESC["servfail"] = 0 -RESSTATDESC["formerr"] = 0 -RESSTATDESC["othererror"] = 0 -RESSTATDESC["edns0fail"] = 0 -RESSTATDESC["mismatch"] = 0 -RESSTATDESC["truncated"] = 0 -RESSTATDESC["lame"] = 0 -RESSTATDESC["retry"] = 0 -RESSTATDESC["dispabort"] = 0 -RESSTATDESC["dispsockfail"] = 0 -RESSTATDESC["querytimeout"] = 0 -RESSTATDESC["gluefetchv4"] = 0 -RESSTATDESC["gluefetchv6"] = 0 -RESSTATDESC["gluefetchv4fail"] = 0 -RESSTATDESC["gluefetchv6fail"] = 0 -RESSTATDESC["val"] = 0 -RESSTATDESC["valsuccess"] = 0 -RESSTATDESC["valnegsuccess"] = 0 -RESSTATDESC["valfail"] = 0 -RESSTATDESC["queryrtt0"] = 0 -RESSTATDESC["queryrtt1"] = 0 -RESSTATDESC["queryrtt2"] = 0 -RESSTATDESC["queryrtt3"] = 0 -RESSTATDESC["queryrtt4"] = 0 -RESSTATDESC["queryrtt5"] = 0 -SOCKSTATDESC={} -SOCKSTATDESC["counterid"] = 0 -SOCKSTATDESC["udp4open"] = 0 -SOCKSTATDESC["udp6open"] = 0 -SOCKSTATDESC["tcp4open"] = 0 -SOCKSTATDESC["tcp6open"] = 0 -SOCKSTATDESC["unixopen"] = 0 -SOCKSTATDESC["udp4openfail"] = 0 -SOCKSTATDESC["udp6openfail"] = 0 -SOCKSTATDESC["tcp4openfail"] = 0 -SOCKSTATDESC["tcp6openfail"] = 0 -SOCKSTATDESC["unixopenfail"] = 0 -SOCKSTATDESC["udp4close"] = 0 -SOCKSTATDESC["udp6close"] = 0 -SOCKSTATDESC["tcp4close"] = 0 -SOCKSTATDESC["tcp6close"] = 0 -SOCKSTATDESC["unixclose"] = 0 -SOCKSTATDESC["fdwatchclose"] = 0 -SOCKSTATDESC["udp4bindfail"] = 0 -SOCKSTATDESC["udp6bindfail"] = 0 -SOCKSTATDESC["tcp4bindfail"] = 0 -SOCKSTATDESC["tcp6bindfail"] = 0 -SOCKSTATDESC["unixbindfail"] = 0 -SOCKSTATDESC["fdwatchbindfail"] = 0 -SOCKSTATDESC["udp4connectfail"] = 0 -SOCKSTATDESC["udp6connectfail"] = 0 -SOCKSTATDESC["tcp4connectfail"] = 0 -SOCKSTATDESC["tcp6connectfail"] = 0 -SOCKSTATDESC["unixconnectfail"] = 0 -SOCKSTATDESC["fdwatchconnectfail"] = 0 -SOCKSTATDESC["udp4connect"] = 0 -SOCKSTATDESC["udp6connect"] = 0 -SOCKSTATDESC["tcp4connect"] = 0 -SOCKSTATDESC["tcp6connect"] = 0 -SOCKSTATDESC["unixconnect"] = 0 -SOCKSTATDESC["fdwatchconnect"] = 0 -SOCKSTATDESC["tcp4acceptfail"] = 0 -SOCKSTATDESC["tcp6acceptfail"] = 0 -SOCKSTATDESC["unixacceptfail"] = 0 -SOCKSTATDESC["tcp4accept"] = 0 -SOCKSTATDESC["tcp6accept"] = 0 -SOCKSTATDESC["unixaccept"] = 0 -SOCKSTATDESC["udp4sendfail"] = 0 -SOCKSTATDESC["udp6sendfail"] = 0 -SOCKSTATDESC["tcp4sendfail"] = 0 -SOCKSTATDESC["tcp6sendfail"] = 0 -SOCKSTATDESC["unixsendfail"] = 0 -SOCKSTATDESC["fdwatchsendfail"] = 0 -SOCKSTATDESC["udp4recvfail"] = 0 -SOCKSTATDESC["udp6recvfail"] = 0 -SOCKSTATDESC["tcp4recvfail"] = 0 -SOCKSTATDESC["tcp6recvfail"] = 0 -SOCKSTATDESC["unixrecvfail"] = 0 -SOCKSTATDESC["fdwatchrecvfail"] = 0 -SYSSTATDESC={} -SYSSTATDESC['sockets'] = 0 -SYSSTATDESC['memory'] = 0 - -sent = -1 -last_sent = -1 -loop = 0 - -while 1: - NSSTATDESC["requestv4"] += random.randint(1,1000) - wait = sent + step_time - time.time() - if wait <= 0: - last_sent = sent; - sent = time.time(); - msg = {'component':'auth', 'version':1, 'timestamp':time.time(),'stats':{'NSSTATDESC':NSSTATDESC,'RESSTATDESC':RESSTATDESC,'SOCKSTATDESC':SOCKSTATDESC,'SYSSTATDESC':SYSSTATDESC}} - print (msg) - print (cc.group_sendmsg(msg, statgroup)) - wait = last_sent + step_time - time.time() - if wait < 0: - wait = step_time - loop += 1 - r,w,e = select.select([cc._socket],[],[], wait) - for sock in r: - if sock == cc._socket: - data,envelope = cc.group_recvmsg(False) - print (data) - if (envelope["group"] == "Boss"): - if ("shutdown" in data): - exit() - else: - print ("Unknown data: ", envelope,data) diff --git a/src/bin/stats/test_total.py b/src/bin/stats/test_total.py deleted file mode 100644 index 7979f9c1e1..0000000000 --- a/src/bin/stats/test_total.py +++ /dev/null @@ -1,54 +0,0 @@ -import sys -sys.path.insert(0, '.') -from statsd import * - -def test_total(): - stats = { - 'auth': { - 'from1': { - 'component':'auth', - 'version':1, - 'from':'from1', - 'timestamp':20100125, - 'stats': { - 'AUTH': { - 'counterid': 1, - 'requestv4': 2, - 'requestv6': 4, - }, - 'SYS': { - 'sockets': 8, - 'memory': 16, - }, - }, - }, - 'from2': { - 'component':'auth', - 'version':1, - 'from':'from1', - 'timestamp':20100126, - 'stats': { - 'AUTH': { - 'counterid': 256, - 'requestv4': 512, - 'requestv6': 1024, - }, - 'SYS': { - 'sockets': 2048, - 'memory': 4096, - }, - }, - }, - }, - }; - t = {} - for key in stats: - t[key] = total(stats[key]) - print (stats) - print (dicttoxml(stats)) - print (t) - print (dicttoxml(t)) - - -if __name__ == "__main__": - test_total() diff --git a/src/bin/stats/tests/Makefile.am b/src/bin/stats/tests/Makefile.am new file mode 100644 index 0000000000..9bfd627cfd --- /dev/null +++ b/src/bin/stats/tests/Makefile.am @@ -0,0 +1,14 @@ +PYTESTS = b10-stats_test.py b10-stats_stub_test.py +EXTRA_DIST = $(PYTESTS) +CLEANFILES = unittest_fakesession.pyc + +# later will have configure option to choose this, like: coverage run --branch +PYCOVERAGE = $(PYTHON) +# test using command-line arguments, so use check-local target instead of TESTS +check-local: + for pytest in $(PYTESTS) ; do \ + echo Running test: $$pytest ; \ + env PYTHONPATH=$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python:$(abs_top_builddir)/src/bin/stats \ + B10_FROM_BUILD=$(abs_top_builddir) \ + $(PYCOVERAGE) $(abs_srcdir)/$$pytest || exit ; \ + done diff --git a/src/bin/stats/tests/b10-stats_stub_test.py b/src/bin/stats/tests/b10-stats_stub_test.py new file mode 100644 index 0000000000..75c5fde843 --- /dev/null +++ b/src/bin/stats/tests/b10-stats_stub_test.py @@ -0,0 +1,116 @@ +# Copyright (C) 2010 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. + +# $Id$ +__version__ = "$Revision$" + +# +# Tests for the stats stub module +# +import unittest +import time +import os +import imp +import stats_stub +from isc.cc.session import Session +from stats_stub import CCSessionStub, BossModuleStub, AuthModuleStub +from stats import get_datetime + +class TestStats(unittest.TestCase): + + def setUp(self): + self.session = Session() + self.stub = CCSessionStub(session=self.session, verbose=True) + self.boss = BossModuleStub(session=self.session, verbose=True) + self.auth = AuthModuleStub(session=self.session, verbose=True) + self.env = {'from': self.session.lname, 'group': 'Stats', + 'instance': '*', 'to':'*', + 'type':'send','seq':0} + self.result_ok = {'result': [0]} + + def tearDown(self): + self.session.close() + + def test_stub(self): + """ + Test for send_command of CCSessionStub object + """ + env = self.env + result_ok = self.result_ok + self.assertEqual(('status', None, env), + self.stub.send_command('status', None)) + self.assertEqual(result_ok, self.session.get_message("Stats", None)) + self.assertEqual(('shutdown', None, env), + self.stub.send_command('shutdown', None)) + self.assertEqual(result_ok, self.session.get_message("Stats", None)) + self.assertEqual(('show', None, env), + self.stub.send_command('show', None)) + self.assertEqual(result_ok, self.session.get_message("Stats", None)) + self.assertEqual(('set', {'atest': 100.0}, env), + self.stub.send_command('set', {'atest': 100.0})) + self.assertEqual(result_ok, self.session.get_message("Stats", None)) + + def test_boss_stub(self): + """ + Test for send_command of BossModuleStub object + """ + env = self.env + result_ok = self.result_ok + self.assertEqual(('set', {"stats_data": + {"bind10.boot_time": get_datetime()} + }, env), self.boss.send_boottime()) + self.assertEqual(result_ok, self.session.get_message("Stats", None)) + + def test_auth_stub(self): + """ + Test for send_command of AuthModuleStub object + """ + env = self.env + result_ok = self.result_ok + self.assertEqual( + ('set', {"stats_data": {"auth.queries.udp": 1}}, env), + self.auth.send_udp_query_count()) + self.assertEqual(result_ok, self.session.get_message("Stats", None)) + self.assertEqual( + ('set', {"stats_data": {"auth.queries.tcp": 1}}, env), + self.auth.send_tcp_query_count()) + self.assertEqual(result_ok, self.session.get_message("Stats", None)) + self.assertEqual( + ('set', {"stats_data": {"auth.queries.udp": 100}}, env), + self.auth.send_udp_query_count(cmd='set', cnt=100)) + self.assertEqual(result_ok, self.session.get_message("Stats", None)) + self.assertEqual( + ('set', {"stats_data": {"auth.queries.tcp": 99}}, env), + self.auth.send_tcp_query_count(cmd='set', cnt=99)) + self.assertEqual(result_ok, self.session.get_message("Stats", None)) + + def test_func_main(self): + # explicitly make failed + self.session.close() + stats_stub.main(session=self.session) + + def test_osenv(self): + """ + test for not having environ "B10_FROM_BUILD" + """ + if "B10_FROM_BUILD" in os.environ: + path = os.environ["B10_FROM_BUILD"] + os.environ.pop("B10_FROM_BUILD") + imp.reload(stats_stub) + os.environ["B10_FROM_BUILD"] = path + imp.reload(stats_stub) + +if __name__ == "__main__": + unittest.main() diff --git a/src/bin/stats/tests/b10-stats_test.py b/src/bin/stats/tests/b10-stats_test.py new file mode 100644 index 0000000000..873c24d5e5 --- /dev/null +++ b/src/bin/stats/tests/b10-stats_test.py @@ -0,0 +1,646 @@ +# Copyright (C) 2010 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. + +# $Id$ +__version__ = "$Revision$" + +# +# Tests for the stats module +# +import os +import sys +import time +import unittest +import imp +from isc.cc.session import Session, SessionError +from isc.config.ccsession import ModuleCCSession, ModuleCCSessionError +import stats +from stats import SessionSubject, CCSessionListener, get_timestamp, get_datetime +from fake_time import _TEST_TIME_SECS, _TEST_TIME_STRF + +# setting Constant +if sys.path[0] == '': + TEST_SPECFILE_LOCATION = "./testdata/stats_test.spec" +else: + TEST_SPECFILE_LOCATION = sys.path[0] + "/testdata/stats_test.spec" + +class TestStats(unittest.TestCase): + + def setUp(self): + self.session = Session() + self.subject = SessionSubject(session=self.session, verbose=True) + self.listener = CCSessionListener(self.subject, verbose=True) + self.stats_spec = self.listener.cc_session.get_module_spec().get_config_spec() + self.module_name = self.listener.cc_session.get_module_spec().get_module_name() + self.stats_data = { + 'report_time' : get_datetime(), + 'bind10.boot_time' : "1970-01-01T00:00:00Z", + 'stats.timestamp' : get_timestamp(), + 'stats.lname' : self.session.lname, + 'auth.queries.tcp': 0, + 'auth.queries.udp': 0, + "stats.boot_time": get_datetime(), + "stats.start_time": get_datetime(), + "stats.last_update_time": get_datetime() + } + # check starting + self.assertFalse(self.subject.running) + self.subject.start() + self.assertTrue(self.subject.running) + self.assertEqual(len(self.session.message_queue), 0) + self.assertEqual(self.module_name, 'Stats') + + def tearDown(self): + # check closing + self.subject.stop() + self.assertFalse(self.subject.running) + self.subject.detach(self.listener) + self.listener.stop() + self.session.close() + + def test_local_func(self): + """ + Test for local function + + """ + # test for result_ok + self.assertEqual(type(result_ok()), dict) + self.assertEqual(result_ok(), {'result': [0]}) + self.assertEqual(result_ok(1), {'result': [1]}) + self.assertEqual(result_ok(0,'OK'), {'result': [0, 'OK']}) + self.assertEqual(result_ok(1,'Not good'), {'result': [1, 'Not good']}) + self.assertEqual(result_ok(None,"It's None"), {'result': [None, "It's None"]}) + self.assertNotEqual(result_ok(), {'RESULT': [0]}) + + # test for get_timestamp + self.assertEqual(get_timestamp(), _TEST_TIME_SECS) + + # test for get_datetime + self.assertEqual(get_datetime(), _TEST_TIME_STRF) + + def test_show_command(self): + """ + Test for show command + + """ + # test show command without arg + self.session.group_sendmsg({"command": [ "show", None ]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + # ignore under 0.9 seconds + self.assertEqual(result_ok(0, self.stats_data), result_data) + self.assertEqual(len(self.session.message_queue), 0) + + # test show command with arg + self.session.group_sendmsg({"command": [ "show", {"stats_item_name": "stats.lname"}]}, "Stats") + self.assertEqual(len(self.subject.session.message_queue), 1) + self.subject.check() + result_data = self.subject.session.get_message("Stats", None) + self.assertEqual(result_ok(0, {'stats.lname': self.stats_data['stats.lname']}), + result_data) + self.assertEqual(len(self.subject.session.message_queue), 0) + + # test show command with arg which has wrong name + self.session.group_sendmsg({"command": [ "show", {"stats_item_name": "stats.dummy"}]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + # ignore under 0.9 seconds + self.assertEqual(result_ok(0, self.stats_data), result_data) + self.assertEqual(len(self.session.message_queue), 0) + + def test_set_command(self): + """ + Test for set command + + """ + # test set command + self.stats_data['auth.queries.udp'] = 54321 + self.assertEqual(self.stats_data['auth.queries.udp'], 54321) + self.assertEqual(self.stats_data['auth.queries.tcp'], 0) + self.session.group_sendmsg({ "command": [ + "set", { + 'stats_data': {'auth.queries.udp': 54321 } + } ] }, + "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + # test show command + self.session.group_sendmsg({"command": [ "show", None ]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertEqual(result_ok(0, self.stats_data), result_data) + self.assertEqual(len(self.session.message_queue), 0) + + # test set command 2 + self.stats_data['auth.queries.udp'] = 0 + self.assertEqual(self.stats_data['auth.queries.udp'], 0) + self.assertEqual(self.stats_data['auth.queries.tcp'], 0) + self.session.group_sendmsg({ "command": [ "set", {'stats_data': {'auth.queries.udp': 0}} ]}, + "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + # test show command 2 + self.session.group_sendmsg({"command": [ "show", None ]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertEqual(result_ok(0, self.stats_data), result_data) + self.assertEqual(len(self.session.message_queue), 0) + + # test set command 3 + self.stats_data['auth.queries.tcp'] = 54322 + self.assertEqual(self.stats_data['auth.queries.udp'], 0) + self.assertEqual(self.stats_data['auth.queries.tcp'], 54322) + self.session.group_sendmsg({ "command": [ + "set", { + 'stats_data': {'auth.queries.tcp': 54322 } + } ] }, + "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + # test show command 3 + self.session.group_sendmsg({"command": [ "show", None ]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertEqual(result_ok(0, self.stats_data), result_data) + self.assertEqual(len(self.session.message_queue), 0) + + def test_remove_command(self): + """ + Test for remove command + + """ + self.session.group_sendmsg({"command": + [ "remove", {"stats_item_name": 'bind10.boot_time' }]}, + "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + self.assertEqual(self.stats_data.pop('bind10.boot_time'), "1970-01-01T00:00:00Z") + self.assertFalse('bind10.boot_time' in self.stats_data) + + # test show command with arg + self.session.group_sendmsg({"command": + [ "show", {"stats_item_name": 'bind10.boot_time'}]}, + "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertFalse('bind10.boot_time' in result_data['result'][1]) + self.assertEqual(result_ok(0, self.stats_data), result_data) + self.assertEqual(len(self.session.message_queue), 0) + + def test_reset_command(self): + """ + Test for reset command + + """ + self.session.group_sendmsg({"command": [ "reset" ] }, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + # test show command + self.session.group_sendmsg({"command": [ "show" ]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertEqual(result_ok(0, self.stats_data), result_data) + self.assertEqual(len(self.session.message_queue), 0) + + def test_status_command(self): + """ + Test for status command + + """ + self.session.group_sendmsg({"command": [ "status" ] }, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(0, "I'm alive."), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + def test_unknown_command(self): + """ + Test for unknown command + + """ + self.session.group_sendmsg({"command": [ "hoge", None ]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(1, "Unknown command: 'hoge'"), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + def test_shutdown_command(self): + """ + Test for shutdown command + + """ + self.session.group_sendmsg({"command": [ "shutdown", None ]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.assertTrue(self.subject.running) + self.subject.check() + self.assertFalse(self.subject.running) + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + + def test_some_commands(self): + """ + Test for some commands in a row + + """ + # test set command + self.stats_data['bind10.boot_time'] = '2010-08-02T14:47:56Z' + self.assertEqual(self.stats_data['bind10.boot_time'], '2010-08-02T14:47:56Z') + self.session.group_sendmsg({ "command": [ + "set", { + 'stats_data': {'bind10.boot_time': '2010-08-02T14:47:56Z' } + }]}, + "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + # check its value + self.session.group_sendmsg({ "command": [ + "show", { 'stats_item_name': 'bind10.boot_time' } + ] }, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertEqual(result_ok(0, {'bind10.boot_time': '2010-08-02T14:47:56Z'}), + result_data) + self.assertEqual(result_ok(0, {'bind10.boot_time': self.stats_data['bind10.boot_time']}), + result_data) + self.assertEqual(len(self.session.message_queue), 0) + + # test set command 2nd + self.stats_data['auth.queries.udp'] = 98765 + self.assertEqual(self.stats_data['auth.queries.udp'], 98765) + self.session.group_sendmsg({ "command": [ + "set", { 'stats_data': { + 'auth.queries.udp': + self.stats_data['auth.queries.udp'] + } } + ] }, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + # check its value + self.session.group_sendmsg({"command": [ + "show", {'stats_item_name': 'auth.queries.udp'} + ] }, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertEqual(result_ok(0, {'auth.queries.udp': 98765}), + result_data) + self.assertEqual(result_ok(0, {'auth.queries.udp': self.stats_data['auth.queries.udp']}), + result_data) + self.assertEqual(len(self.session.message_queue), 0) + + # test set command 3 + self.stats_data['auth.queries.tcp'] = 4321 + self.session.group_sendmsg({"command": [ + "set", + {'stats_data': {'auth.queries.tcp': 4321 }} ]}, + "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + # check value + self.session.group_sendmsg({"command": [ "show", {'stats_item_name': 'auth.queries.tcp'} ]}, + "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertEqual(result_ok(0, {'auth.queries.tcp': 4321}), + result_data) + self.assertEqual(result_ok(0, {'auth.queries.tcp': self.stats_data['auth.queries.tcp']}), + result_data) + self.assertEqual(len(self.session.message_queue), 0) + + self.session.group_sendmsg({"command": [ "show", {'stats_item_name': 'auth.queries.udp'} ]}, + "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertEqual(result_ok(0, {'auth.queries.udp': 98765}), + result_data) + self.assertEqual(result_ok(0, {'auth.queries.udp': self.stats_data['auth.queries.udp']}), + result_data) + self.assertEqual(len(self.session.message_queue), 0) + + # test set command 4 + self.stats_data['auth.queries.tcp'] = 67890 + self.session.group_sendmsg({"command": [ + "set", {'stats_data': {'auth.queries.tcp': 67890 }} ]}, + "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + # test show command for all values + self.session.group_sendmsg({"command": [ "show", None ]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertEqual(result_ok(0, self.stats_data), result_data) + self.assertEqual(len(self.session.message_queue), 0) + + def test_some_commands2(self): + """ + Test for some commands in a row using list-type value + + """ + self.stats_data['listtype'] = [1, 2, 3] + self.assertEqual(self.stats_data['listtype'], [1, 2, 3]) + self.session.group_sendmsg({ "command": [ + "set", {'stats_data': {'listtype': [1, 2, 3] }} + ]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + # check its value + self.session.group_sendmsg({ "command": [ + "show", { 'stats_item_name': 'listtype'} + ]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertEqual(result_ok(0, {'listtype': [1, 2, 3]}), + result_data) + self.assertEqual(result_ok(0, {'listtype': self.stats_data['listtype']}), + result_data) + self.assertEqual(len(self.session.message_queue), 0) + + # test set list-type value + self.assertEqual(self.stats_data['listtype'], [1, 2, 3]) + self.session.group_sendmsg({"command": [ + "set", {'stats_data': {'listtype': [3, 2, 1, 0] }} + ]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + # check its value + self.session.group_sendmsg({ "command": [ + "show", { 'stats_item_name': 'listtype' } + ] }, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertEqual(result_ok(0, {'listtype': [3, 2, 1, 0]}), + result_data) + self.assertEqual(len(self.session.message_queue), 0) + + def test_some_commands3(self): + """ + Test for some commands in a row using dictionary-type value + + """ + self.stats_data['dicttype'] = {"a": 1, "b": 2, "c": 3} + self.assertEqual(self.stats_data['dicttype'], {"a": 1, "b": 2, "c": 3}) + self.session.group_sendmsg({ "command": [ + "set", { + 'stats_data': {'dicttype': {"a": 1, "b": 2, "c": 3} } + }]}, + "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + # check its value + self.session.group_sendmsg({ "command": [ "show", { 'stats_item_name': 'dicttype' } ]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertEqual(result_ok(0, {'dicttype': {"a": 1, "b": 2, "c": 3}}), + result_data) + self.assertEqual(result_ok(0, {'dicttype': self.stats_data['dicttype']}), + result_data) + self.assertEqual(len(self.session.message_queue), 0) + + # test set list-type value + self.assertEqual(self.stats_data['dicttype'], {"a": 1, "b": 2, "c": 3}) + self.session.group_sendmsg({"command": [ + "set", {'stats_data': {'dicttype': {"a": 3, "b": 2, "c": 1, "d": 0} }} ]}, + "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + self.assertEqual(len(self.session.message_queue), 0) + + # check its value + self.session.group_sendmsg({ "command": [ "show", { 'stats_item_name': 'dicttype' }]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + result_data = self.session.get_message("Stats", None) + self.assertEqual(result_ok(0, {'dicttype': {"a": 3, "b": 2, "c": 1, "d": 0} }), + result_data) + self.assertEqual(len(self.session.message_queue), 0) + + def test_config_update(self): + """ + Test for config update + + """ + # test show command without arg + self.session.group_sendmsg({"command": [ "config_update", {"x-version":999} ]}, "Stats") + self.assertEqual(len(self.session.message_queue), 1) + self.subject.check() + self.assertEqual(result_ok(), + self.session.get_message("Stats", None)) + +class TestStats2(unittest.TestCase): + + def setUp(self): + self.session = Session(verbose=True) + self.subject = SessionSubject(session=self.session, verbose=True) + self.listener = CCSessionListener(self.subject, verbose=True) + self.module_name = self.listener.cc_session.get_module_spec().get_module_name() + # check starting + self.assertFalse(self.subject.running) + self.subject.start() + self.assertTrue(self.subject.running) + self.assertEqual(len(self.session.message_queue), 0) + self.assertEqual(self.module_name, 'Stats') + + def tearDown(self): + # check closing + self.subject.stop() + self.assertFalse(self.subject.running) + self.subject.detach(self.listener) + self.listener.stop() + + def test_specfile(self): + """ + Test for specfile + + """ + if "B10_FROM_BUILD" in os.environ: + self.assertEqual(stats.SPECFILE_LOCATION, + os.environ["B10_FROM_BUILD"] + "/src/bin/stats/stats.spec") + imp.reload(stats) + # change path of SPECFILE_LOCATION + stats.SPECFILE_LOCATION = TEST_SPECFILE_LOCATION + self.assertEqual(stats.SPECFILE_LOCATION, TEST_SPECFILE_LOCATION) + self.subject = stats.SessionSubject(session=self.session, verbose=True) + self.session = self.subject.session + self.listener = stats.CCSessionListener(self.subject, verbose=True) + + self.assertEqual(self.listener.stats_spec, []) + self.assertEqual(self.listener.stats_data, {}) + + self.assertEqual(self.listener.commands_spec, [ + { + "command_name": "status", + "command_description": "identify whether stats module is alive or not", + "command_args": [] + }, + { + "command_name": "the_dummy", + "command_description": "this is for testing", + "command_args": [] + }]) + + def test_func_initialize_data(self): + """ + Test for initialize_data function + + """ + # prepare for sample data set + stats_spec = [ + { + "item_name": "none_sample", + "item_type": "null", + "item_default": "None" + }, + { + "item_name": "boolean_sample", + "item_type": "boolean", + "item_default": True + }, + { + "item_name": "string_sample", + "item_type": "string", + "item_default": "A something" + }, + { + "item_name": "int_sample", + "item_type": "integer", + "item_default": 9999999 + }, + { + "item_name": "real_sample", + "item_type": "real", + "item_default": 0.0009 + }, + { + "item_name": "list_sample", + "item_type": "list", + "item_default": [0, 1, 2, 3, 4], + "list_item_spec": [] + }, + { + "item_name": "map_sample", + "item_type": "map", + "item_default": {'name':'value'}, + "map_item_spec": [] + }, + { + "item_name": "other_sample", + "item_type": "__unknown__", + "item_default": "__unknown__" + } + ] + # data for comparison + stats_data = { + 'none_sample': None, + 'boolean_sample': True, + 'string_sample': 'A something', + 'int_sample': 9999999, + 'real_sample': 0.0009, + 'list_sample': [0, 1, 2, 3, 4], + 'map_sample': {'name':'value'}, + 'other_sample': '__unknown__' + } + self.assertEqual(self.listener.initialize_data(stats_spec), stats_data) + + def test_func_main(self): + # explicitly make failed + self.session.close() + stats.main(session=self.session) + + def test_osenv(self): + """ + test for not having environ "B10_FROM_BUILD" + """ + if "B10_FROM_BUILD" in os.environ: + path = os.environ["B10_FROM_BUILD"] + os.environ.pop("B10_FROM_BUILD") + imp.reload(stats) + os.environ["B10_FROM_BUILD"] = path + imp.reload(stats) + +def result_ok(*args): + if args: + return { 'result': list(args) } + else: + return { 'result': [ 0 ] } + +if __name__ == "__main__": + unittest.main() diff --git a/src/bin/stats/tests/fake_time.py b/src/bin/stats/tests/fake_time.py new file mode 100644 index 0000000000..964097f36b --- /dev/null +++ b/src/bin/stats/tests/fake_time.py @@ -0,0 +1,48 @@ +# Copyright (C) 2010 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. + +# $Id$ +__version__ = "$Revision$" + +# This is a dummy time class against a Python standard time class. +# It is just testing use only. +# Other methods which time class has is not implemented. +# (This class isn't orderloaded for time class.) + +# These variables are constant. These are example. +_TEST_TIME_SECS = 1283364938.229088 +_TEST_TIME_STRF = '2010-09-01T18:15:38Z' + +def time(): + """ + This is a dummy time() method against time.time() + """ + # return float constant value + return _TEST_TIME_SECS + +def gmtime(): + """ + This is a dummy gmtime() method against time.gmtime() + """ + # always return nothing + return None + +def strftime(*arg): + """ + This is a dummy gmtime() method against time.gmtime() + """ + return _TEST_TIME_STRF + + diff --git a/src/bin/stats/tests/isc/__init__.py b/src/bin/stats/tests/isc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/bin/stats/tests/isc/cc/__init__.py b/src/bin/stats/tests/isc/cc/__init__.py new file mode 100644 index 0000000000..9a3eaf6185 --- /dev/null +++ b/src/bin/stats/tests/isc/cc/__init__.py @@ -0,0 +1 @@ +from isc.cc.session import * diff --git a/src/bin/stats/tests/isc/cc/session.py b/src/bin/stats/tests/isc/cc/session.py new file mode 100644 index 0000000000..ab9c29639d --- /dev/null +++ b/src/bin/stats/tests/isc/cc/session.py @@ -0,0 +1,127 @@ +# Copyright (C) 2010 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. + +# $Id$ +# This module is a mock-up class of isc.cc.session + +__version__ = "$Revision$" + +import sys + +# set a dummy lname +_TEST_LNAME = '123abc@xxxx' + +class Queue(): + def __init__(self, msg=None, env={}): + self.msg = msg + self.env = env + + def dump(self): + return { 'msg': self.msg, 'env': self.env } + +class SessionError(Exception): + pass + +class Session: + def __init__(self, socket_file=None, verbose=False): + self._lname = _TEST_LNAME + self.message_queue = [] + self.old_message_queue = [] + self._socket = True + self.verbose = verbose + + @property + def lname(self): + return self._lname + + def close(self): + self._socket = False + + def _next_sequence(self, que=None): + return len(self.message_queue) + + def enqueue(self, msg=None, env={}): + if not self._socket: + raise SessionError("Session has been closed.") + seq = self._next_sequence() + env.update({"seq": 0}) # fixed here + que = Queue(msg=msg, env=env) + self.message_queue.append(que) + if self.verbose: + sys.stdout.write("[Session] enqueue: " + str(que.dump()) + "\n") + return seq + + def dequeue(self, seq=0): + if not self._socket: + raise SessionError("Session has been closed.") + que = None + try: + que = self.message_queue.pop(seq) + self.old_message_queue.append(que) + except IndexError: + que = Queue() + if self.verbose: + sys.stdout.write("[Session] dequeue: " + str(que.dump()) + "\n") + return que + + def get_queue(self, seq=None): + if not self._socket: + raise SessionError("Session has been closed.") + if seq is None: + seq = len(self.message_queue) - 1 + que = None + try: + que = self.message_queue[seq] + except IndexError: + raise IndexError + que = Queue() + if self.verbose: + sys.stdout.write("[Session] get_queue: " + str(que.dump()) + "\n") + return que + + def group_sendmsg(self, msg, group, instance="*", to="*"): + return self.enqueue(msg=msg, env={ + "type": "send", + "from": self._lname, + "to": to, + "group": group, + "instance": instance }) + + def group_recvmsg(self, nonblock=True, seq=0): + que = self.dequeue(seq) + return que.msg, que.env + + def group_reply(self, routing, msg): + return self.enqueue(msg=msg, env={ + "type": "send", + "from": self._lname, + "to": routing["from"], + "group": routing["group"], + "instance": routing["instance"], + "reply": routing["seq"] }) + + def get_message(self, group, to='*'): + if not self._socket: + raise SessionError("Session has been closed.") + que = Queue() + for q in self.message_queue: + if q.env['group'] == group: + self.message_queue.remove(q) + self.old_message_queue.append(q) + que = q + if self.verbose: + sys.stdout.write("[Session] get_message: " + str(que.dump()) + "\n") + return q.msg + diff --git a/src/bin/stats/tests/isc/config/__init__.py b/src/bin/stats/tests/isc/config/__init__.py new file mode 100644 index 0000000000..4c49e956aa --- /dev/null +++ b/src/bin/stats/tests/isc/config/__init__.py @@ -0,0 +1 @@ +from isc.config.ccsession import * diff --git a/src/bin/stats/tests/isc/config/ccsession.py b/src/bin/stats/tests/isc/config/ccsession.py new file mode 100644 index 0000000000..b47f51854e --- /dev/null +++ b/src/bin/stats/tests/isc/config/ccsession.py @@ -0,0 +1,114 @@ +# Copyright (C) 2010 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. + +# $Id$ +# This module is a mock-up class of isc.cc.session + +__version__ = "$Revision$" + +import json +from isc.cc.session import Session + +COMMAND_CONFIG_UPDATE = "config_update" + +def parse_answer(msg): + try: + return msg['result'][0], msg['result'][1] + except IndexError: + return msg['result'][0], None + +def create_answer(rcode, arg = None): + if arg is None: + return { 'result': [ rcode ] } + else: + return { 'result': [ rcode, arg ] } + +def parse_command(msg): + try: + return msg['command'][0], msg['command'][1] + except IndexError: + return msg['command'][0], None + +def create_command(command_name, params = None): + if params is None: + return {"command": [command_name]} + else: + return {"command": [command_name, params]} + +def module_spec_from_file(spec_file, check = True): + file = open(spec_file) + module_spec = json.loads(file.read()) + return ModuleSpec(module_spec['module_spec'], check) + +class ModuleSpec: + def __init__(self, module_spec, check = True): + self._module_spec = module_spec + + def get_config_spec(self): + return self._module_spec['config_data'] + + def get_commands_spec(self): + return self._module_spec['commands'] + + def get_module_name(self): + return self._module_spec['module_name'] + +class ModuleCCSessionError(Exception): + pass + +class ConfigData: + def __init__(self, specification): + self.specification = specification + +class ModuleCCSession(ConfigData): + def __init__(self, spec_file_name, config_handler, command_handler, cc_session = None): + module_spec = module_spec_from_file(spec_file_name) + ConfigData.__init__(self, module_spec) + self._module_name = module_spec.get_module_name() + self.set_config_handler(config_handler) + self.set_command_handler(command_handler) + if not cc_session: + self._session = Session(verbose=True) + else: + self._session = cc_session + + def start(self): + pass + + def close(self): + self._session.close() + + def check_command(self): + msg, env = self._session.group_recvmsg(False) + if not msg or 'result' in msg: + return + cmd, arg = parse_command(msg) + answer = None + if cmd == COMMAND_CONFIG_UPDATE and self._config_handler: + answer = self._config_handler(arg) + elif env['group'] == self._module_name and self._command_handler: + answer = self._command_handler(cmd, arg) + if answer: + self._session.group_reply(env, answer) + + def set_config_handler(self, config_handler): + self._config_handler = config_handler + # should we run this right now since we've changed the handler? + + def set_command_handler(self, command_handler): + self._command_handler = command_handler + + def get_module_spec(self): + return self.specification diff --git a/src/bin/stats/tests/isc/utils/__init__.py b/src/bin/stats/tests/isc/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/bin/stats/tests/isc/utils/process.py b/src/bin/stats/tests/isc/utils/process.py new file mode 100644 index 0000000000..806c2ae5ec --- /dev/null +++ b/src/bin/stats/tests/isc/utils/process.py @@ -0,0 +1,20 @@ +# Copyright (C) 2010 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. + +# $Id$ + +# A dummy function of isc.utils.process.rename() +def rename(name=None): + pass diff --git a/src/bin/stats/tests/stats_test.in b/src/bin/stats/tests/stats_test.in new file mode 100644 index 0000000000..ae031d949a --- /dev/null +++ b/src/bin/stats/tests/stats_test.in @@ -0,0 +1,31 @@ +#! /bin/sh + +# Copyright (C) 2010 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. + +PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@} +export PYTHON_EXEC + +PYTHONPATH=@abs_top_builddir@/src/lib/python:@abs_top_srcdir@/src/bin/stats +export PYTHONPATH + +B10_FROM_BUILD=@abs_top_builddir@ +export B10_FROM_BUILD + +TEST_PATH=@abs_top_srcdir@/src/bin/stats/tests + +cd ${TEST_PATH} +${PYTHON_EXEC} -O b10-stats_test.py $* +${PYTHON_EXEC} -O b10-stats_stub_test.py $* diff --git a/src/bin/stats/tests/testdata/stats_test.spec b/src/bin/stats/tests/testdata/stats_test.spec new file mode 100644 index 0000000000..8136756440 --- /dev/null +++ b/src/bin/stats/tests/testdata/stats_test.spec @@ -0,0 +1,19 @@ +{ + "module_spec": { + "module_name": "Stats", + "module_description": "Stats daemon", + "config_data": [], + "commands": [ + { + "command_name": "status", + "command_description": "identify whether stats module is alive or not", + "command_args": [] + }, + { + "command_name": "the_dummy", + "command_description": "this is for testing", + "command_args": [] + } + ] + } +} diff --git a/src/lib/python/isc/config/module_spec.py b/src/lib/python/isc/config/module_spec.py index 7c98017fe0..1793cb608b 100644 --- a/src/lib/python/isc/config/module_spec.py +++ b/src/lib/python/isc/config/module_spec.py @@ -316,7 +316,9 @@ def _validate_spec(spec, full, data, errors): item_name = spec['item_name'] item_optional = spec['item_optional'] - if item_name in data: + if not data and item_optional: + return True + elif item_name in data: return _validate_item(spec, full, data[item_name], errors) elif full and not item_optional: if errors != None: