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 + '%s>\n' %k
- else:
- output += spaces + '<%s>%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: