Browse Source

[trac521] Summary of changes

- add new BIND 10 module "b10-stats-httpd"
  + a standalone HTTP server for the statistics HTTP/XML interface
  + b10-stats-httpd responds statistics XML data to clients.
  + b10-stats-httpd request statistics data to b10-stats, and converts 
    python dict format to XML format.
  + It also responds XSD and XSL documents with XML data.

  + main of b10-stats-httpd
    stats_httpd.py.in
    run_b10-stats-httpd.sh.in

  + spec file related to b10-stats-httpd
    stats-httpd.spec.in

  + unittests for b10-stats-httpd
    b10-stats-httpd_test.py

  + documents related to b10-stats-httpd
    b10-stats-httpd.8      
    b10-stats-httpd.xml

  + XML/XSD/XSL template files which b10-stats-httpd reads
    stats-httpd-xml.tpl.in
    stats-httpd-xsd.tpl.in  
    stats-httpd-xsl.tpl.in

  + dummy python modules for testing b10-stats-httpd
    http.server
    select
    socket

  + b10-stats-httpd is added in:
    configure.ac
    bind10_test.py.in
    bind10.py.in

- change existent dummy modules for testing b10-stats and b10-stats-httpd
  + isc.cc.session
  + isc.config.ccsession

- change environment variable
  from: "B10_FROM_BUILD"
  to  : "B10_FROM_SOURCE"

- rename spec file
  from: src/bin/stats/stats.spec.pre.in
  to:   src/bin/stats/stats.spec.in

- remove unnecessary module "b10-stats_stub"
  stats_stub.py.in
  run_b10-stats_stub.sh.in
  b10-stats_stub_test.py

- add miscellaneous changes
  stats_test.in
  b10-stats_test.py

- add a proposed entry for this branch
  ChangeLog
Naoki Kambe 14 years ago
parent
commit
7a7299fb09

+ 11 - 0
ChangeLog

@@ -1,3 +1,14 @@
+  nnn.  [func]		naokikambe
+	Added the HTTP/XML interface(b10-stats-httpd) to the statistics feature
+	in BIND 10. b10-stats-httpd is a standalone HTTP server and it requests
+	statistics data to the stats daemon(b10-stats) and sends it to HTTP
+	clients in XML format. Items of the data collected via b10-stats-httpd
+	are almost equivalent to ones which are collected via bindctl. Since it
+	also can send XSL(Extensible Stylessheet Language) document and XSD(XML
+	Schema definition) document, XML document is human-friendly to view
+	through web browsers and its data types are strictly defined.
+	(Trac #547, git tbdtbdtbdtbdtbdtbdtbdtbdtbdtbdtbdtbdttbd)
+
   197.  [bug]		zhang likun
   197.  [bug]		zhang likun
 	Remove expired message and rrset entries when looking up them
 	Remove expired message and rrset entries when looking up them
 	in cache, touch or remove the rrset entry in cache properly
 	in cache, touch or remove the rrset entry in cache properly

+ 9 - 4
configure.ac

@@ -637,6 +637,7 @@ AC_CONFIG_FILES([Makefile
                  src/bin/stats/tests/isc/config/Makefile
                  src/bin/stats/tests/isc/config/Makefile
                  src/bin/stats/tests/isc/util/Makefile
                  src/bin/stats/tests/isc/util/Makefile
                  src/bin/stats/tests/testdata/Makefile
                  src/bin/stats/tests/testdata/Makefile
+                 src/bin/stats/tests/http/Makefile
                  src/bin/usermgr/Makefile
                  src/bin/usermgr/Makefile
                  src/bin/tests/Makefile
                  src/bin/tests/Makefile
                  src/lib/Makefile
                  src/lib/Makefile
@@ -713,10 +714,14 @@ AC_OUTPUT([doc/version.ent
            src/bin/zonemgr/tests/zonemgr_test
            src/bin/zonemgr/tests/zonemgr_test
            src/bin/zonemgr/run_b10-zonemgr.sh
            src/bin/zonemgr/run_b10-zonemgr.sh
            src/bin/stats/stats.py
            src/bin/stats/stats.py
-           src/bin/stats/stats_stub.py
-           src/bin/stats/stats.spec.pre
+           src/bin/stats/stats_httpd.py
+           src/bin/stats/stats.spec
+           src/bin/stats/stats-httpd.spec
+           src/bin/stats/stats-httpd-xml.tpl
+           src/bin/stats/stats-httpd-xsd.tpl
+           src/bin/stats/stats-httpd-xsl.tpl
            src/bin/stats/run_b10-stats.sh
            src/bin/stats/run_b10-stats.sh
-           src/bin/stats/run_b10-stats_stub.sh
+           src/bin/stats/run_b10-stats-httpd.sh
            src/bin/stats/tests/stats_test
            src/bin/stats/tests/stats_test
            src/bin/bind10/bind10.py
            src/bin/bind10/bind10.py
            src/bin/bind10/tests/bind10_test
            src/bin/bind10/tests/bind10_test
@@ -759,7 +764,7 @@ AC_OUTPUT([doc/version.ent
            chmod +x src/bin/zonemgr/run_b10-zonemgr.sh
            chmod +x src/bin/zonemgr/run_b10-zonemgr.sh
            chmod +x src/bin/stats/tests/stats_test
            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.sh
-           chmod +x src/bin/stats/run_b10-stats_stub.sh
+           chmod +x src/bin/stats/run_b10-stats-httpd.sh
            chmod +x src/bin/bind10/run_bind10.sh
            chmod +x src/bin/bind10/run_bind10.sh
            chmod +x src/bin/cmdctl/tests/cmdctl_test
            chmod +x src/bin/cmdctl/tests/cmdctl_test
            chmod +x src/bin/xfrin/tests/xfrin_test
            chmod +x src/bin/xfrin/tests/xfrin_test

+ 5 - 0
src/bin/bind10/bind10.py.in

@@ -499,6 +499,9 @@ class BoB:
     def start_stats(self, c_channel_env):
     def start_stats(self, c_channel_env):
         self.start_simple("b10-stats", c_channel_env)
         self.start_simple("b10-stats", c_channel_env)
 
 
+    def start_stats_httpd(self, c_channel_env):
+        self.start_simple("b10-stats-httpd", c_channel_env)
+
     def start_cmdctl(self, c_channel_env):
     def start_cmdctl(self, c_channel_env):
         # XXX: we hardcode port 8080
         # XXX: we hardcode port 8080
         self.start_simple("b10-cmdctl", c_channel_env, 8080)
         self.start_simple("b10-cmdctl", c_channel_env, 8080)
@@ -543,6 +546,7 @@ class BoB:
 
 
         # ... and finally start the remaining processes
         # ... and finally start the remaining processes
         self.start_stats(c_channel_env)
         self.start_stats(c_channel_env)
+        self.start_stats_httpd(c_channel_env)
         self.start_cmdctl(c_channel_env)
         self.start_cmdctl(c_channel_env)
     
     
     def startup(self):
     def startup(self):
@@ -592,6 +596,7 @@ class BoB:
         self.cc_session.group_sendmsg(cmd, "Xfrin", "Xfrin")
         self.cc_session.group_sendmsg(cmd, "Xfrin", "Xfrin")
         self.cc_session.group_sendmsg(cmd, "Zonemgr", "Zonemgr")
         self.cc_session.group_sendmsg(cmd, "Zonemgr", "Zonemgr")
         self.cc_session.group_sendmsg(cmd, "Stats", "Stats")
         self.cc_session.group_sendmsg(cmd, "Stats", "Stats")
+        self.cc_session.group_sendmsg(cmd, "StatsHttpd", "StatsHttpd")
 
 
     def stop_process(self, process, recipient):
     def stop_process(self, process, recipient):
         """
         """

+ 8 - 0
src/bin/bind10/tests/bind10_test.py.in

@@ -124,6 +124,7 @@ class StartStopCheckBob(BoB):
         self.xfrin = False
         self.xfrin = False
         self.zonemgr = False
         self.zonemgr = False
         self.stats = False
         self.stats = False
+        self.stats_httpd = False
         self.cmdctl = False
         self.cmdctl = False
         self.c_channel_env = {}
         self.c_channel_env = {}
 
 
@@ -158,6 +159,9 @@ class StartStopCheckBob(BoB):
     def start_stats(self, c_channel_env):
     def start_stats(self, c_channel_env):
         self.stats = True
         self.stats = True
 
 
+    def start_stats_httpd(self, c_channel_env):
+        self.stats_httpd = True
+
     def start_cmdctl(self, c_channel_env):
     def start_cmdctl(self, c_channel_env):
         self.cmdctl = True
         self.cmdctl = True
 
 
@@ -191,6 +195,9 @@ class StartStopCheckBob(BoB):
     def stop_stats(self):
     def stop_stats(self):
         self.stats = False
         self.stats = False
 
 
+    def stop_stats_httpd(self):
+        self.stats_httpd = False
+
     def stop_cmdctl(self):
     def stop_cmdctl(self):
         self.cmdctl = False
         self.cmdctl = False
 
 
@@ -216,6 +223,7 @@ class TestStartStopProcessesBob(unittest.TestCase):
         self.assertEqual(bob.xfrin, auth)
         self.assertEqual(bob.xfrin, auth)
         self.assertEqual(bob.zonemgr, auth)
         self.assertEqual(bob.zonemgr, auth)
         self.assertEqual(bob.stats, core)
         self.assertEqual(bob.stats, core)
+        self.assertEqual(bob.stats_httpd, core)
         self.assertEqual(bob.cmdctl, core)
         self.assertEqual(bob.cmdctl, core)
 
 
     def check_preconditions(self, bob):
     def check_preconditions(self, bob):

+ 14 - 13
src/bin/stats/Makefile.am

@@ -2,35 +2,36 @@ SUBDIRS = tests
 
 
 pkglibexecdir = $(libexecdir)/@PACKAGE@
 pkglibexecdir = $(libexecdir)/@PACKAGE@
 
 
-pkglibexec_SCRIPTS = b10-stats
-noinst_SCRIPTS = b10-stats_stub
+pkglibexec_SCRIPTS = b10-stats b10-stats-httpd
 
 
 b10_statsdir = $(pkgdatadir)
 b10_statsdir = $(pkgdatadir)
-b10_stats_DATA = stats.spec
+b10_stats_DATA = stats.spec stats-httpd.spec
+b10_stats_DATA += stats-httpd-xml.tpl stats-httpd-xsd.tpl stats-httpd-xsl.tpl
 
 
-CLEANFILES = stats.spec b10-stats stats.pyc stats.pyo b10-stats_stub stats_stub.pyc stats_stub.pyo
+CLEANFILES = b10-stats stats.pyc
+CLEANFILES += b10-stats-httpd stats_httpd.pyc
 
 
-man_MANS = b10-stats.8
-EXTRA_DIST = $(man_MANS) b10-stats.xml
+man_MANS = b10-stats.8 b10-stats-httpd.8
+EXTRA_DIST = $(man_MANS) b10-stats.xml b10-stats-httpd.xml
+EXTRA_DIST += stats.spec stats-httpd.spec
+EXTRA_DIST += stats-httpd-xml.tpl stats-httpd-xsd.tpl stats-httpd-xsl.tpl
 
 
 if ENABLE_MAN
 if ENABLE_MAN
 
 
 b10-stats.8: b10-stats.xml
 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
 	xsltproc --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-stats.xml
 
 
-endif
+b10-stats-httpd.8: b10-stats-httpd.xml
+	xsltproc --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-stats-httpd.xml
 
 
-stats.spec: stats.spec.pre
-	$(SED) -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" stats.spec.pre >$@
+endif
 
 
 # this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix
 # this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix
 b10-stats: stats.py
 b10-stats: stats.py
 	$(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" \
 	$(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" \
-	       -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" \
 	       -e "s|.*#@@REMOVED@@$$||"  stats.py >$@
 	       -e "s|.*#@@REMOVED@@$$||"  stats.py >$@
 	chmod a+x $@
 	chmod a+x $@
 
 
-b10-stats_stub: stats_stub.py stats.py
-	$(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" \
-	       -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" stats_stub.py >$@
+b10-stats-httpd: stats_httpd.py
+	$(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" stats_httpd.py >$@
 	chmod a+x $@
 	chmod a+x $@

File diff suppressed because it is too large
+ 133 - 0
src/bin/stats/b10-stats-httpd.8


+ 208 - 0
src/bin/stats/b10-stats-httpd.xml

@@ -0,0 +1,208 @@
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+               "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
+	       [<!ENTITY mdash "&#8212;">]>
+<!--
+ - Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+ -
+ - Permission to use, copy, modify, and/or 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 ISC DISCLAIMS ALL WARRANTIES WITH
+ - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ - AND FITNESS.  IN NO EVENT SHALL ISC 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.
+-->
+
+<refentry>
+
+  <refentryinfo>
+    <date>Mar 8, 2011</date>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>b10-stats-httpd</refentrytitle>
+    <manvolnum>8</manvolnum>
+    <refmiscinfo>BIND10</refmiscinfo>
+  </refmeta>
+
+  <refnamediv>
+    <refname>b10-stats-httpd</refname>
+    <refpurpose>BIND 10 HTTP server for HTTP/XML interface of statistics</refpurpose>
+  </refnamediv>
+
+  <docinfo>
+    <copyright>
+      <year>2011</year>
+      <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
+    </copyright>
+  </docinfo>
+
+  <refsynopsisdiv>
+    <cmdsynopsis>      <command>b10-stats-httpd</command>
+      <arg><option>-v</option></arg>|<arg><option>--verbose</option></arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>DESCRIPTION</title>
+    <para>
+      <command>b10-stats-httpd</command> is a standalone HTTP server. It is
+      intended for HTTP/XML interface for statistics module. This server
+      process runs as a process separated from the process of the BIND 10 Stats
+      daemon (<command>b10-stats</command>). The server is initially executed
+      by the BIND 10 boss process (<command>bind10</command>) and finally
+      exited by it.  The server is intended to be requested by HTTP clients
+      like web browsers and third-party modules. When the server is requested,
+      it requests BIND 10 statistics data to <command>b10-stats</command>,
+      and <command>b10-stats</command> sends the data to the server in Python
+      dictionary format and the server converts it into XML format. The server
+      sends it to the HTTP client. The server can send three types of document,
+      which are XML (Extensible Markup Language), XSD(XML Schema definition)
+      and XSL(Extensible Stylesheet Language). The XML document is the
+      statistics data of BIND 10, The XSD document is the data schema of it,
+      and The XSL document is the style sheet to be showed for the web
+      browsers. Please see the manual and the spec file
+      of <command>b10-stats</command> for more details about the items of BIND
+      10 statistics. The server uses CC session in communication
+      with <command>b10-stats</command>. CC session is provided
+      by <command>b10-msgq</command> which is started
+      by <command>bind10</command> in advance. The server is implemented by
+      HTTP-server libraries included in Python 3. The parameter of the server
+      is a list of pairs of the listening address and the port number specified
+      in the spec file of it. The server obtains the configuration from the
+      config manager (<command>b10-cfgmgr</command>) in runtime. Please see
+      below for more details about this spec file and configuration of the
+      server.
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>OPTIONS</title>
+    <para>The arguments are as follows:</para>
+    <variablelist>
+      <varlistentry>
+        <term><option>-v</option>, <option>--verbose</option></term>
+        <listitem>
+	  <para>
+          <command>b10-stats-httpd</command> switches to verbose mode and sends
+          verbose messages to STDOUT.
+	  </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>FILES</title>
+    <para>
+      <filename>/usr/local/share/bind10-devel/stats-httpd.spec</filename>
+      &mdash; the spec file of <command>b10-stats-httpd</command>. This file
+      contains configurable settings
+      of <command>b10-stats-httpd</command>. This setting can be configured in
+      runtime via
+      <refentrytitle>bindctl</refentrytitle><manvolnum>1</manvolnum>. Please
+      see the manual
+      of <refentrytitle>bindctl</refentrytitle><manvolnum>1</manvolnum> about
+      how to configure the settings.
+    </para>
+    <para>
+      <filename>/usr/local/share/bind10-devel/stats-httpd-xml.tpl</filename>
+      &mdash; the template file of XML document.
+    </para>
+    <para>
+      <filename>/usr/local/share/bind10-devel/stats-httpd-xsd.tpl</filename>
+      &mdash; the template file of XSD document.
+    </para>
+    <para>
+      <filename>/usr/local/share/bind10-devel/stats-httpd-xsl.tpl</filename>
+      &mdash; the template file of XSL document.
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>CONFIGURATION AND COMMANDS</title>
+    <para>
+      The configurable setting in 
+      <filename>stats-httpd.spec</filename> is:
+    </para>
+    <variablelist>
+      <varlistentry>
+        <term><varname>listen_on</varname></term>
+        <listitem>
+	  <para>
+      	    a list of pairs of address and port for
+	    <command>b10-stats-httpd</command> to listen HTTP requests on. The
+	    pair is consist of the <varname>address</varname> string
+	    and <varname>port</varname> number. The default setting is the list
+	    of address 127.0.0.1 port 8000. If the server is started by the
+	    default setting being left, for example, the URL for XML document
+	    is http://127.0.0.1:8000/bind10/statistics/xml. And also IPv6
+	    addresses can be configured and they works in the runtime
+	    environment for dual stack.
+	  </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+    <para>
+      The configuration commands in <filename>stats-httpd.spec</filename> are:
+    </para>
+    <variablelist>
+      <varlistentry>
+        <term><command>status</command></term>
+        <listitem>
+	  <para>
+	    shows the status of <command>b10-stats-httpd</command> with its
+	    PID on the runtime machine.
+	  </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term><command>shutdown</command></term>
+        <listitem>
+	  <para>
+	    exits the <command>b10-stats-httpd</command> process. (Note that
+	    the BIND 10 boss process will restart this service.)
+	  </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>SEE ALSO</title>
+    <para>
+      <citerefentry>
+        <refentrytitle>b10-stats</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>,
+      <citerefentry>
+        <refentrytitle>b10-msgq</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>,
+      <citerefentry>
+        <refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>,
+      <citerefentry>
+        <refentrytitle>bind10</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>,
+      <citerefentry>
+	<refentrytitle>bindctl</refentrytitle><manvolnum>1</manvolnum>
+      </citerefentry>,
+      <citetitle>BIND 10 Guide</citetitle>.
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>HISTORY</title>
+    <para>
+      <command>b10-stats-httpd</command> was designed and implemented by Naoki
+      Kambe of JPRS in Mar 2011.
+    </para>
+  </refsect1>
+</refentry><!--
+ - Local variables:
+ - mode: sgml
+ - End:
+-->

+ 5 - 5
src/bin/stats/run_b10-stats_stub.sh.in

@@ -1,6 +1,6 @@
 #! /bin/sh
 #! /bin/sh
 
 
-# Copyright (C) 2010  Internet Systems Consortium.
+# Copyright (C) 2011  Internet Systems Consortium.
 #
 #
 # Permission to use, copy, modify, and distribute this software for any
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
 # purpose with or without fee is hereby granted, provided that the above
@@ -21,13 +21,13 @@ export PYTHON_EXEC
 PYTHONPATH=@abs_top_builddir@/src/lib/python
 PYTHONPATH=@abs_top_builddir@/src/lib/python
 export PYTHONPATH
 export PYTHONPATH
 
 
-B10_FROM_BUILD=@abs_top_srcdir@
-export B10_FROM_BUILD
-
 BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
 BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
 export BIND10_MSGQ_SOCKET_FILE
 export BIND10_MSGQ_SOCKET_FILE
 
 
 STATS_PATH=@abs_top_builddir@/src/bin/stats
 STATS_PATH=@abs_top_builddir@/src/bin/stats
 
 
+B10_FROM_SOURCE=@abs_top_srcdir@/src/bin/stats
+export B10_FROM_SOURCE
+
 cd ${STATS_PATH}
 cd ${STATS_PATH}
-exec ${PYTHON_EXEC} -O b10-stats_stub "$@"
+exec ${PYTHON_EXEC} -O b10-stats-httpd "$@"

+ 3 - 3
src/bin/stats/run_b10-stats.sh.in

@@ -1,6 +1,6 @@
 #! /bin/sh
 #! /bin/sh
 
 
-# Copyright (C) 2010  Internet Systems Consortium.
+# Copyright (C) 2010, 2011  Internet Systems Consortium.
 #
 #
 # Permission to use, copy, modify, and distribute this software for any
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
 # purpose with or without fee is hereby granted, provided that the above
@@ -24,8 +24,8 @@ export PYTHONPATH
 BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
 BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
 export BIND10_MSGQ_SOCKET_FILE
 export BIND10_MSGQ_SOCKET_FILE
 
 
-B10_FROM_BUILD=@abs_top_builddir@
-export B10_FROM_BUILD
+B10_FROM_SOURCE=@abs_top_srcdir@/src/bin/stats
+export B10_FROM_SOURCE
 
 
 STATS_PATH=@abs_top_builddir@/src/bin/stats
 STATS_PATH=@abs_top_builddir@/src/bin/stats
 
 

+ 24 - 0
src/bin/stats/stats-httpd-xml.tpl.in

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="$xsl_url_path"?>
+<!--
+ - Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+ -
+ - Permission to use, copy, modify, and/or 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 ISC DISCLAIMS ALL WARRANTIES WITH
+ - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ - AND FITNESS.  IN NO EVENT SHALL ISC 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.
+-->
+
+<stats:stats_data version="1.0"
+  xmlns:stats="$xsd_namespace"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="$xsd_namespace $xsd_url_path">
+  $xml_string
+</stats:stats_data>

+ 38 - 0
src/bin/stats/stats-httpd-xsd.tpl.in

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ - Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+ -
+ - Permission to use, copy, modify, and/or 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 ISC DISCLAIMS ALL WARRANTIES WITH
+ - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ - AND FITNESS.  IN NO EVENT SHALL ISC 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.
+-->
+
+<schema targetNamespace="$xsd_namespace"
+  xmlns="http://www.w3.org/2001/XMLSchema"
+  xmlns:stats="$xsd_namespace">
+  <annotation>
+    <documentation xml:lang="en">XML schema of the statistics
+      data in BIND 10</documentation>
+  </annotation>
+  <element name="stats_data">
+    <annotation>
+      <documentation>A set of statistics data</documentation>
+    </annotation>
+    <complexType>
+      $xsd_string
+      <attribute name="version" type="token" use="optional" default="1.0">
+        <annotation>
+          <documentation>Version number of syntax</documentation>
+        </annotation>
+      </attribute>
+    </complexType>
+  </element>
+</schema>

+ 56 - 0
src/bin/stats/stats-httpd-xsl.tpl.in

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ - Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+ -
+ - Permission to use, copy, modify, and/or 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 ISC DISCLAIMS ALL WARRANTIES WITH
+ - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ - AND FITNESS.  IN NO EVENT SHALL ISC 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.
+-->
+
+<xsl:stylesheet version="1.0"
+  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml"
+  xmlns:stats="$xsd_namespace">
+  <xsl:output method="html" encoding="UTF-8"
+    doctype-public="-//W3C//DTD HTML 4.01 Transitional//EN"
+    doctype-system=" http://www.w3.org/TR/html4/loose.dtd " />
+  <xsl:template match="/">
+    <html lang="en">
+      <head>
+        <title>BIND 10 Statistics</title>
+        <style type="text/css"><![CDATA[
+table {
+  border: 1px #000000 solid;
+  border-collapse: collapse;
+}
+td, th {
+  padding: 3px 20px;
+  border: 1px #000000 solid;
+}
+td.title {
+  text-decoration:underline;
+}
+]]>
+        </style>
+      </head>
+      <body>
+        <h1>BIND 10 Statistics</h1>
+        <table>
+          <tr>
+            <th>Title</th>
+            <th>Value</th>
+          </tr>
+          <xsl:apply-templates />
+        </table>
+      </body>
+    </html>
+  </xsl:template>
+  $xsl_string
+</xsl:stylesheet>

+ 54 - 0
src/bin/stats/stats-httpd.spec.in

@@ -0,0 +1,54 @@
+{
+  "module_spec": {
+    "module_name": "StatsHttpd",
+    "module_description": "Stats HTTP daemon",
+    "config_data": [
+      {
+        "item_name": "listen_on",
+        "item_type": "list",
+        "item_optional": false,
+        "item_default": [
+          {
+            "address": "127.0.0.1",
+            "port": 8000
+          }
+        ],
+        "list_item_spec": {
+          "item_name": "address",
+          "item_type": "map",
+          "item_optional": false,
+          "item_default": {},
+          "map_item_spec": [
+            {
+              "item_name": "address",
+              "item_type": "string",
+              "item_optional": true,
+              "item_default": "127.0.0.1",
+              "item_description": "listen-on address for HTTP"
+            },
+            {
+              "item_name": "port",
+              "item_type": "integer",
+              "item_optional": true,
+              "item_default": 8000,
+              "item_description": "listen-on port for HTTP"
+            }
+          ]
+        },
+        "item_description": "http listen-on address and port"
+      }
+    ],
+    "commands": [
+      {
+        "command_name": "status",
+        "command_description": "Status of the stats httpd",
+        "command_args": []
+      },
+      {
+        "command_name": "shutdown",
+        "command_description": "Shut down the stats httpd",
+        "command_args": []
+      }
+    ]
+  }
+}

+ 6 - 7
src/bin/stats/stats.py.in

@@ -1,6 +1,6 @@
 #!@PYTHON@
 #!@PYTHON@
 
 
-# Copyright (C) 2010  Internet Systems Consortium.
+# Copyright (C) 2010, 2011  Internet Systems Consortium.
 #
 #
 # Permission to use, copy, modify, and distribute this software for any
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
 # purpose with or without fee is hereby granted, provided that the above
@@ -15,8 +15,6 @@
 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
 
-__version__ = "$Revision$"
-
 import sys; sys.path.append ('@@PYTHONPATH@@')
 import sys; sys.path.append ('@@PYTHONPATH@@')
 import os
 import os
 import signal
 import signal
@@ -37,15 +35,16 @@ if __name__ == 'stats':					#@@REMOVED@@
 import isc.util.process
 import isc.util.process
 isc.util.process.rename()
 isc.util.process.rename()
 
 
-# If B10_FROM_BUILD is set in the environment, we use data files
+# If B10_FROM_SOURCE is set in the environment, we use data files
 # from a directory relative to that, otherwise we use the ones
 # from a directory relative to that, otherwise we use the ones
 # installed on the system
 # installed on the system
-if "B10_FROM_BUILD" in os.environ:
-    SPECFILE_LOCATION = os.environ["B10_FROM_BUILD"] + "/src/bin/stats/stats.spec"
+if "B10_FROM_SOURCE" in os.environ:
+    SPECFILE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + "stats.spec"
 else:
 else:
     PREFIX = "@prefix@"
     PREFIX = "@prefix@"
     DATAROOTDIR = "@datarootdir@"
     DATAROOTDIR = "@datarootdir@"
-    SPECFILE_LOCATION = "@datadir@/@PACKAGE@/stats.spec".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
+    SPECFILE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@" + os.sep + "stats.spec"
+    SPECFILE_LOCATION = SPECFILE_LOCATION.replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
 
 
 class Singleton(type):
 class Singleton(type):
     """
     """

+ 1 - 1
src/bin/stats/stats.spec.pre.in

@@ -17,7 +17,7 @@
         "item_type": "string",
         "item_type": "string",
         "item_optional": false,
         "item_optional": false,
         "item_default": "1970-01-01T00:00:00Z",
         "item_default": "1970-01-01T00:00:00Z",
-        "item_title": "stats.BootTime",
+        "item_title": "bind10.BootTime",
         "item_description": "A date time when bind10 process starts initially",
         "item_description": "A date time when bind10 process starts initially",
         "item_format": "date-time"
         "item_format": "date-time"
       },
       },

+ 502 - 0
src/bin/stats/stats_httpd.py.in

@@ -0,0 +1,502 @@
+#!@PYTHON@
+
+# Copyright (C) 2011  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import sys; sys.path.append ('@@PYTHONPATH@@')
+import os
+import time
+import errno
+import select
+from optparse import OptionParser, OptionValueError
+import http.server
+import socket
+import string
+import xml.etree.ElementTree
+
+import isc.cc
+import isc.config
+import isc.util.process
+
+# If B10_FROM_SOURCE is set in the environment, we use data files
+# from a directory relative to that, otherwise we use the ones
+# installed on the system
+if "B10_FROM_SOURCE" in os.environ:
+    BASE_LOCATION = os.environ["B10_FROM_SOURCE"]
+else:
+    PREFIX = "@prefix@"
+    DATAROOTDIR = "@datarootdir@"
+    BASE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@"
+    BASE_LOCATION = BASE_LOCATION.replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
+SPECFILE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd.spec"
+STATS_SPECFILE_LOCATION = BASE_LOCATION + os.sep + "stats.spec"
+XML_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xml.tpl"
+XSD_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xsd.tpl"
+XSL_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xsl.tpl"
+
+# These variables are paths part of URL.
+# eg. "http://${address}" + XXX_URL_PATH
+XML_URL_PATH = '/bind10/statistics/xml'
+XSD_URL_PATH = '/bind10/statistics/xsd'
+XSL_URL_PATH = '/bind10/statistics/xsl'
+# TODO: This should be considered later.
+XSD_NAMESPACE = 'http://bind10.isc.org' + XSD_URL_PATH
+DEFAULT_CONFIG = dict(listen_on=[('127.0.0.1', 8000)])
+
+# The size of template file should be under 100k. TODO: should be
+# considered later.
+MAX_SIZE_OF_TEMPLATE = 102400
+
+# Assign this process name
+isc.util.process.rename()
+
+class HttpHandler(http.server.BaseHTTPRequestHandler):
+    """HTTP handler class for HttpServer class. The class inhrits the super
+    class http.server.BaseHTTPRequestHandler. It implemets do_GET()
+    and do_HEAD() and orverrides log_message()"""
+    def do_GET(self):
+        body = self.send_head()
+        if body is not None:
+            self.wfile.write(body.encode())
+
+    def do_HEAD(self):
+        self.send_head()
+
+    def send_head(self):
+        assert isinstance(self.server, HttpServer)
+        try:
+            if self.path == XML_URL_PATH:
+                body = self.server.xml_handler()
+            elif self.path == XSD_URL_PATH:
+                body = self.server.xsd_handler()
+            elif self.path == XSL_URL_PATH:
+                body = self.server.xsl_handler()
+            else:
+                if 'Host' in self.headers.keys():
+                    # redirect to XML URL
+                    self.send_response(302)
+                    self.send_header(
+                        "Location",
+                        "http://" + self.headers.get('Host') + XML_URL_PATH)
+                    self.end_headers()
+                    return None
+                else:
+                    # Couldn't find HOST
+                    self.send_error(404)
+                    return None
+        except StatsHttpdError as err:
+            self.send_error(500)
+            if self.server.verbose:
+                self.server.log_writer(
+                    "[b10-stats-httpd] %s\n" % err)
+            return None
+        else:
+            self.send_response(200)
+            self.send_header("Content-type", "text/xml")
+            self.send_header("Content-Length", len(body))
+            self.end_headers()
+            return body
+
+    def log_message(self, format, *args):
+        """Change the default log format"""
+        assert isinstance(self.server, HttpServer)
+        if self.server.verbose:
+            self.server.log_writer(
+                "[b10-stats-httpd] %s - - [%s] %s\n" %
+                (self.address_string(),
+                 self.log_date_time_string(),
+                 format%args))
+
+class HttpServerError(Exception):
+    """Exception class for HttpServer class. It is intended to be
+    passed from the HttpServer object to the StatsHttpd object."""
+    pass
+
+class HttpServer(http.server.HTTPServer):
+    """HTTP Server class. The class inherits the super
+    http.server.HTTPServer. Some parameters are specified as
+    arguments, which are xml_handler, xsd_handler, xsl_handler, and
+    log_writer. These all are parameters which the StatsHttpd object
+    has. The handler parameters are references of functions which
+    return body of each document. The last parameter log_writer is
+    reference of writer function to just write to
+    sys.stderr.write. They are intended to be referred by HttpHandler
+    object."""
+    def __init__(self, server_address, handler,
+                 xml_handler, xsd_handler, xsl_handler, log_writer, verbose=False):
+        self.server_address = server_address
+        self.xml_handler = xml_handler
+        self.xsd_handler = xsd_handler
+        self.xsl_handler = xsl_handler
+        self.log_writer = log_writer
+        self.verbose = verbose
+        http.server.HTTPServer.__init__(self, server_address, handler)
+
+class StatsHttpdError(Exception):
+    """Exception class for StatsHttpd class. It is intended to be
+    thrown from the the StatsHttpd object to the HttpHandler object or
+    main routine."""
+    pass
+
+class StatsHttpd:
+    """The main class of HTTP server of HTTP/XML interface for
+    statistics module. It handles HTTP requests, and command channel
+    and config channel CC session. It uses select.select function
+    while waiting for clients requests."""
+    def __init__(self, verbose=False):
+        self.verbose = verbose
+        self.running = False
+        self.poll_intval = 0.5
+        self.write_log = sys.stderr.write
+        self.mccs = None
+        self.httpd = []
+        self.open_mccs()
+        self.load_config()
+        self.load_templates()
+        self.open_httpd()
+
+    def open_mccs(self):
+        """Opens a ModuleCCSession object"""
+        # create ModuleCCSession
+        if self.verbose:
+            self.write_log("[b10-stats-httpd] Starting CC Session\n")
+        self.mccs = isc.config.ModuleCCSession(
+            SPECFILE_LOCATION, self.config_handler, self.command_handler)
+        self.cc_session = self.mccs._session
+        # read spec file of stats module and subscribe 'Stats'
+        self.stats_module_spec = isc.config.module_spec_from_file(STATS_SPECFILE_LOCATION)
+        self.stats_config_spec = self.stats_module_spec.get_config_spec()
+        self.stats_module_name = self.stats_module_spec.get_module_name()
+        if self.verbose:
+            self.write_log("[b10-stats-httpd] Starts to subscribe stats module\n")
+        self.cc_session.group_subscribe(self.stats_module_name, "*")
+
+    def close_mccs(self):
+        """Closes a ModuleCCSession object"""
+        if self.mccs is None:
+            return
+        if self.verbose:
+            self.write_log("[b10-stats-httpd] Closing CC Session\n")
+        try:
+            self.cc_session.group_unsubscribe(self.stats_module_name, "*")
+        except isc.cc.session.SessionError as se:
+            if self.verbose:
+                self.write_log("[b10-stats-httpd] Failed to unsubscribe stats module\n")
+        self.mccs.close()
+        self.mccs = None
+
+    def load_config(self, new_config={}):
+        """Loads configuration from spec file or new configuration
+        from the config manager"""
+        # load config
+        if len(new_config) > 0:
+            assert type(self.config) is dict
+            assert type(new_config) is dict
+            self.config.update(new_config)
+        else:
+            self.config = DEFAULT_CONFIG
+            self.config.update(
+                dict([
+                        (itm['item_name'], self.mccs.get_value(itm['item_name'])[0])
+                        for itm in self.mccs.get_module_spec().get_config_spec()
+                        ])
+                )
+        assert 'listen_on' in self.config
+        assert type(self.config['listen_on']) is list
+        # remove duplicated element
+        self.http_addrs = list(
+            set([ (cf['address'], cf['port']) for cf in self.config['listen_on'] ])
+                )
+
+    def open_httpd(self):
+        """Opens sockets for HTTP. Iterating each HTTP address to be
+        configured in spec file"""
+        assert type(self.http_addrs) is list
+        for addr in self.http_addrs:
+            self.httpd.append(self._open_httpd(addr))
+
+    def _open_httpd(self, server_address, address_family=None):
+        assert type(server_address) is tuple
+        try:
+            # try IPv6 at first
+            if address_family is not None:
+                HttpServer.address_family = address_family
+            elif socket.has_ipv6:
+                HttpServer.address_family = socket.AF_INET6
+            httpd = HttpServer(
+                server_address, HttpHandler,
+                self.xml_handler, self.xsd_handler, self.xsl_handler,
+                self.write_log, self.verbose)
+        except (socket.gaierror, socket.error,
+                OverflowError, TypeError) as err:
+            # try IPv4 next
+            if HttpServer.address_family == socket.AF_INET6:
+                httpd = self._open_httpd(server_address, socket.AF_INET)
+            else:
+                raise HttpServerError(
+                    "Invalid address %s, port %s: %s: %s" %
+                    (server_address[0], server_address[1],
+                     err.__class__.__name__, err))
+        else:
+            if self.verbose:
+                self.write_log(
+                    "[b10-stats-httpd] Started on address %s, port %s\n" %
+                    server_address)
+        assert isinstance(httpd, HttpServer)
+        return httpd
+
+    def close_httpd(self):
+        """Closes sockets for HTTP"""
+        if len(self.httpd) == 0:
+            return
+        for ht in self.httpd:
+            assert type(ht.server_address) is tuple
+            if self.verbose:
+                self.write_log(
+                    "[b10-stats-httpd] Closing address %s, port %s\n" %
+                    (ht.server_address[0], ht.server_address[1])
+                    )
+            ht.server_close()
+        self.httpd = []
+
+    def start(self):
+        """Starts StatsHttpd objects to run. Waiting for client
+        requests by using select.select functions"""
+        self.mccs.start()
+        self.running = True
+        while self.running:
+            try:
+                (rfd, wfd, xfd) = select.select(
+                    self.get_sockets(), [], [], self.poll_intval)
+            except select.error as err:
+                if err.args[0] == errno.EINTR:
+                    (rfd, wfd, xfd) = ([], [], [])
+                else:
+                    raise select.error(err)
+            for fd in rfd + xfd:
+                if fd == self.mccs.get_socket():
+                    self.mccs.check_command(nonblock=False)
+                    continue
+                for ht in self.httpd:
+                    if fd == ht.socket:
+                        ht.handle_request()
+                        break
+        self.stop()
+
+    def stop(self):
+        """Stops the running StatsHttpd objects. Closes CC session and
+        HTTP handling sockets"""
+        if self.verbose:
+            self.write_log("[b10-stats-httpd] Shutting down\n")
+        self.close_httpd()
+        self.close_mccs()
+
+    def get_sockets(self):
+        """Returns sockets to select.select"""
+        sockets = []
+        if self.mccs is not None:
+            sockets.append(self.mccs.get_socket())
+        if len(self.httpd) > 0:
+            for ht in self.httpd:
+                sockets.append(ht.socket)
+        return sockets
+
+    def config_handler(self, new_config):
+        """Config handler for the ModuleCCSession object. It resets
+        addresses and ports to listen HTTP requests on."""
+        assert type(new_config) is dict
+        if self.verbose:
+            self.write_log("[b10-stats-httpd] Loading config : %s\n" % str(new_config))
+        for key in new_config.keys():
+            if key not in DEFAULT_CONFIG:
+                if self.verbose:
+                    self.write_log(
+                        "[b10-stats-httpd] Unknown known config: %s" % key)
+                return isc.config.ccsession.create_answer(
+                    1, "Unknown known config: %s" % key)
+        # backup old config
+        old_config = self.config.copy()
+        self.close_httpd()
+        self.load_config(new_config)
+        try:
+            self.open_httpd()
+        except HttpServerError as err:
+            if self.verbose:
+                self.write_log("[b10-stats-httpd] %s\n" % err)
+                self.write_log("[b10-stats-httpd] Restoring old config\n")
+            # restore old config
+            self.config_handler(old_config)
+            return isc.config.ccsession.create_answer(
+                1, "[b10-stats-httpd] %s" % err)
+        else:
+            return isc.config.ccsession.create_answer(0)
+
+    def command_handler(self, command, args):
+        """Command handler for the ModuleCCSesson object. It handles
+        "status" and "shutdown" commands."""
+        if command == "status":
+            if self.verbose:
+                self.write_log("[b10-stats-httpd] Received 'status' command\n")
+            return isc.config.ccsession.create_answer(
+                0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")")
+        elif command == "shutdown":
+            if self.verbose:
+                self.write_log("[b10-stats-httpd] Received 'shutdown' command\n")
+            self.running = False
+            return isc.config.ccsession.create_answer(
+                0, "Stats Httpd is shutting down.")
+        else:
+            if self.verbose:
+                self.write_log("[b10-stats-httpd] Received unknown command\n")
+            return isc.config.ccsession.create_answer(
+                1, "Unknown command: " + str(command))
+
+    def get_stats_data(self):
+        """Requests statistics data to the Stats daemon and returns
+        the data which obtains from it"""
+        try:
+            seq = self.cc_session.group_sendmsg(
+                isc.config.ccsession.create_command('show'),
+                self.stats_module_name)
+            (answer, env) = self.cc_session.group_recvmsg(False, seq)
+            if answer:
+                (rcode, value) = isc.config.ccsession.parse_answer(answer)
+                self.cc_session.group_reply(
+                    env, isc.config.ccsession.create_answer(0))
+        except (isc.cc.session.SessionTimeout,
+                isc.cc.session.SessionError) as err:
+            raise StatsHttpdError("%s: %s" %
+                                  (err.__class__.__name__, err))
+        else:
+            if rcode == 0:
+                return value
+            else:
+                raise StatsHttpdError("Stats module: %s" % str(value))
+
+    def get_stats_spec(self):
+        """Just returns spec data"""
+        return self.stats_config_spec
+
+    def load_templates(self):
+        """Setup the bodies of XSD and XSL documents to be responds to
+        HTTP clients. Before that it also creates XML tag structures by
+        using xml.etree.ElementTree.Element class and substitutes
+        concrete strings with parameters embed in the string.Template
+        object."""
+        # for XSD
+        xsd_root = xml.etree.ElementTree.Element("all") # started with "all" tag
+        for item in self.get_stats_spec():
+            element = xml.etree.ElementTree.Element(
+                "element",
+                dict( name=item["item_name"],
+                      type=item["item_type"] if item["item_type"].lower() != 'real' else 'float',
+                      minOccurs="1",
+                      maxOccurs="1" ),
+                )
+            annotation = xml.etree.ElementTree.Element("annotation")
+            appinfo = xml.etree.ElementTree.Element("appinfo")
+            documentation = xml.etree.ElementTree.Element("documentation")
+            appinfo.text = item["item_title"]
+            documentation.text = item["item_description"]
+            annotation.append(appinfo)
+            annotation.append(documentation)
+            element.append(annotation)
+            xsd_root.append(element)
+        xsd_string = xml.etree.ElementTree.tostring(xsd_root)
+        self.xsd_body = self.open_template(XSD_TEMPLATE_LOCATION).substitute(
+            xsd_string=xsd_string,
+            xsd_namespace=XSD_NAMESPACE
+            )
+        assert self.xsd_body is not None
+
+        # for XSL
+        xsd_root = xml.etree.ElementTree.Element(
+            "xsl:template",
+            dict(match="*")) # started with xml:template tag
+        for item in self.get_stats_spec():
+            tr = xml.etree.ElementTree.Element("tr")
+            td1 = xml.etree.ElementTree.Element(
+                "td", { "class" : "title",
+                        "title" : item["item_description"] })
+            td1.text = item["item_title"]
+            td2 = xml.etree.ElementTree.Element("td")
+            xsl_valueof = xml.etree.ElementTree.Element(
+                "xsl:value-of",
+                dict(select=item["item_name"]))
+            td2.append(xsl_valueof)
+            tr.append(td1)
+            tr.append(td2)
+            xsd_root.append(tr)
+        xsl_string = xml.etree.ElementTree.tostring(xsd_root)
+        self.xsl_body = self.open_template(XSL_TEMPLATE_LOCATION).substitute(
+            xsl_string=xsl_string,
+            xsd_namespace=XSD_NAMESPACE)
+        assert self.xsl_body is not None
+
+    def xml_handler(self):
+        """Handler which requests to Stats daemon to obtain statistics
+        data and returns the body of XML document"""
+        xml_list=[]
+        for (k, v) in self.get_stats_data().items():
+            (k, v) = (str(k), str(v))
+            elem = xml.etree.ElementTree.Element(k)
+            elem.text = v
+            xml_list.append(
+                xml.etree.ElementTree.tostring(elem))
+        assert len(xml_list) > 0
+        xml_string = "".join(xml_list)
+        self.xml_body = self.open_template(XML_TEMPLATE_LOCATION).substitute(
+            xml_string=xml_string,
+            xsd_namespace=XSD_NAMESPACE,
+            xsd_url_path=XSD_URL_PATH,
+            xsl_url_path=XSL_URL_PATH)
+        assert self.xml_body is not None
+        return self.xml_body
+
+    def xsd_handler(self):
+        """Handler which just returns the body of XSD document"""
+        return self.xsd_body
+
+    def xsl_handler(self):
+        """Handler which just returns the body of XSL document"""
+        return self.xsl_body
+
+    def open_template(self, file_name):
+        """Opens a template file with size limitation, loads all lines to a
+        string variable and returns string.Template object include the
+        variable."""
+        lines = "".join(
+            open(file_name, 'r').readlines(MAX_SIZE_OF_TEMPLATE))
+        assert lines is not None
+        return string.Template(lines)
+
+if __name__ == "__main__":
+    try:
+        parser = OptionParser()
+        parser.add_option(
+            "-v", "--verbose", dest="verbose", action="store_true",
+            help="display more about what is going on")
+        (options, args) = parser.parse_args()
+        stats_httpd = StatsHttpd(verbose=options.verbose)
+        stats_httpd.start()
+    except OptionValueError:
+        sys.stderr.write("[b10-stats-httpd] Error parsing options\n")
+    except isc.cc.session.SessionError as se:
+        sys.stderr.write("[b10-stats-httpd] Error creating module, "
+                         + "is the command channel daemon running?\n")
+    except HttpServerError as hse:
+        sys.stderr.write("[b10-stats-httpd] %s\n" % hse)
+    except KeyboardInterrupt as kie:
+        sys.stderr.write("[b10-stats-httpd] Interrupted, exiting\n")

+ 0 - 154
src/bin/stats/stats_stub.py.in

@@ -1,154 +0,0 @@
-#!@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.
-
-__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.util.process
-isc.util.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()

+ 5 - 5
src/bin/stats/tests/Makefile.am

@@ -1,8 +1,8 @@
-SUBDIRS = isc testdata
+SUBDIRS = isc http testdata
 PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
 PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-PYTESTS = b10-stats_test.py b10-stats_stub_test.py
-EXTRA_DIST = $(PYTESTS) fake_time.py
-CLEANFILES = fake_time.pyc
+PYTESTS = b10-stats_test.py b10-stats-httpd_test.py
+EXTRA_DIST = $(PYTESTS) fake_time.py socket.py select.py
+CLEANFILES = fake_time.pyc socket.pyc select.pyc
 
 
 # test using command-line arguments, so use check-local target instead of TESTS
 # test using command-line arguments, so use check-local target instead of TESTS
 check-local:
 check-local:
@@ -14,6 +14,6 @@ endif
 	for pytest in $(PYTESTS) ; do \
 	for pytest in $(PYTESTS) ; do \
 	echo Running test: $$pytest ; \
 	echo Running test: $$pytest ; \
 	env PYTHONPATH=$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python:$(abs_top_builddir)/src/bin/stats:$(abs_top_builddir)/src/bin/stats/tests \
 	env PYTHONPATH=$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python:$(abs_top_builddir)/src/bin/stats:$(abs_top_builddir)/src/bin/stats/tests \
-	B10_FROM_BUILD=$(abs_top_builddir) \
+	B10_FROM_SOURCE=$(abs_top_srcdir)/src/bin/stats \
 	$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
 	$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
 	done
 	done

+ 400 - 0
src/bin/stats/tests/b10-stats-httpd_test.py

@@ -0,0 +1,400 @@
+# Copyright (C) 2011  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import unittest
+import os
+import http.server
+import string
+import select
+import imp
+import sys
+import socket
+
+import isc.cc
+
+import stats_httpd
+
+DUMMY_DATA = {
+    "auth.queries.tcp": 10000,
+    "auth.queries.udp": 12000,
+    "bind10.boot_time": "2011-03-04T11:59:05Z",
+    "report_time": "2011-03-04T11:59:19Z",
+    "stats.boot_time": "2011-03-04T11:59:06Z",
+    "stats.last_update_time": "2011-03-04T11:59:07Z",
+    "stats.lname": "4d70d40a_c@host",
+    "stats.start_time": "2011-03-04T11:59:06Z",
+    "stats.timestamp": 1299239959.560846
+    }
+
+def push_answer(stats_httpd):
+    stats_httpd.cc_session.group_sendmsg(
+        { 'result': 
+          [ 0, DUMMY_DATA ] }, "Stats")
+
+def pull_query(stats_httpd):
+    (msg, env) = stats_httpd.cc_session.group_recvmsg()
+    if 'result' in msg:
+        (ret, arg) = isc.config.ccsession.parse_answer(msg)
+    else:
+        (ret, arg) = isc.config.ccsession.parse_command(msg)
+    return (ret, arg, env)
+
+class TestHttpHandler(unittest.TestCase):
+    """Tests for HttpHandler class"""
+
+    def setUp(self):
+        self.verbose = True
+        self.stats_httpd = stats_httpd.StatsHttpd(self.verbose)
+        self.assertTrue(type(self.stats_httpd.httpd) is list)
+        self.httpd = self.stats_httpd.httpd
+        for ht in self.httpd:
+            self.assertTrue(ht.verbose)
+        self.stats_httpd.cc_session.verbose = False
+
+    def test_do_GET(self):
+        for ht in self.httpd:
+            self._test_do_GET(ht._handler)
+
+    def _test_do_GET(self, handler):
+
+        # URL is '/bind10/statistics/xml'
+        handler.path = stats_httpd.XML_URL_PATH
+        push_answer(self.stats_httpd)
+        handler.do_GET()
+        (ret, arg, env) = pull_query(self.stats_httpd)
+        self.assertEqual(ret, "show")
+        self.assertIsNone(arg)
+        self.assertTrue('group' in env)
+        self.assertEqual(env['group'], 'Stats')
+        (ret, arg, env) = pull_query(self.stats_httpd)
+        self.assertEqual(ret, 0)
+        self.assertIsNone(arg)
+        self.assertTrue('group' in env)
+        self.assertEqual(env['group'], 'Stats')
+        self.assertEqual(handler.response.code, 200)
+        self.assertEqual(handler.response.headers["Content-type"], "text/xml")
+        self.assertTrue(handler.response.headers["Content-Length"] > 0)
+        self.assertTrue(handler.response.wrote_headers)
+        self.assertRegexpMatches(handler.response.body, stats_httpd.XSD_NAMESPACE)
+        self.assertRegexpMatches(handler.response.body, stats_httpd.XSD_URL_PATH)
+        for (k, v) in DUMMY_DATA.items():
+            self.assertRegexpMatches(handler.response.body, str(k))
+            self.assertRegexpMatches(handler.response.body, str(v))
+
+        # URL is '/bind10/statitics/xsd'
+        handler.path = stats_httpd.XSD_URL_PATH
+        handler.do_GET()
+        self.assertEqual(handler.response.code, 200)
+        self.assertEqual(handler.response.headers["Content-type"], "text/xml")
+        self.assertTrue(handler.response.headers["Content-Length"] > 0)
+        self.assertTrue(handler.response.wrote_headers)
+        self.assertRegexpMatches(handler.response.body, stats_httpd.XSD_NAMESPACE)
+        for (k, v) in DUMMY_DATA.items():
+            self.assertRegexpMatches(handler.response.body, str(k))
+
+        # URL is '/bind10/statitics/xsl'
+        handler.path = stats_httpd.XSL_URL_PATH
+        handler.do_GET()
+        self.assertEqual(handler.response.code, 200)
+        self.assertEqual(handler.response.headers["Content-type"], "text/xml")
+        self.assertTrue(handler.response.headers["Content-Length"] > 0)
+        self.assertTrue(handler.response.wrote_headers)
+        self.assertRegexpMatches(handler.response.body, stats_httpd.XSD_NAMESPACE)
+        for (k, v) in DUMMY_DATA.items():
+            self.assertRegexpMatches(handler.response.body, str(k))
+
+        # 302 redirect
+        handler.path = '/'
+        handler.headers = {'Host': 'my.host.domain'}
+        handler.do_GET()
+        self.assertEqual(handler.response.code, 302)
+        self.assertEqual(handler.response.headers["Location"],
+                         "http://my.host.domain%s" % stats_httpd.XML_URL_PATH)
+
+        # 404 NotFound
+        handler.path = '/path/to/foo/bar'
+        handler.headers = {}
+        handler.do_GET()
+        self.assertEqual(handler.response.code, 404)
+
+        # failure case(connection with Stats is down)
+        handler.path = stats_httpd.XML_URL_PATH
+        push_answer(self.stats_httpd)
+        self.assertFalse(self.stats_httpd.cc_session._socket._closed)
+        self.stats_httpd.cc_session._socket._closed = True
+        handler.do_GET()
+        self.stats_httpd.cc_session._socket._closed = False
+        self.assertEqual(handler.response.code, 500)
+        self.stats_httpd.cc_session._clear_ques()
+
+        # failure case(Stats module returns err)
+        handler.path = stats_httpd.XML_URL_PATH
+        self.stats_httpd.cc_session.group_sendmsg(
+            { 'result': [ 1, "I have an error." ] }, "Stats")
+        self.assertFalse(self.stats_httpd.cc_session._socket._closed)
+        self.stats_httpd.cc_session._socket._closed = False
+        handler.do_GET()
+        self.assertEqual(handler.response.code, 500)
+        self.stats_httpd.cc_session._clear_ques()
+
+    def test_do_HEAD(self):
+        for ht in self.httpd:
+            self._test_do_HEAD(ht._handler)
+
+    def _test_do_HEAD(self, handler):
+        handler.path = '/path/to/foo/bar'
+        handler.do_HEAD()
+        self.assertEqual(handler.response.code, 404)
+
+    def test_log_message(self):
+        for ht in self.httpd:
+            self._test_log_message(ht._handler)
+
+    def _test_log_message(self, handler):
+        # switch write_log function
+        handler.server.log_writer = handler.response._write_log
+        log_message = 'ABCDEFG'
+        handler.log_message("%s", log_message)
+        self.assertEqual(handler.response.log, 
+                         "[b10-stats-httpd] %s - - [%s] %s\n" %
+                         (handler.address_string(),
+                          handler.log_date_time_string(),
+                          log_message))
+
+class TestHttpServerError(unittest.TestCase):
+    """Tests for HttpServerError exception"""
+
+    def test_raises(self):
+        try:
+            raise stats_httpd.HttpServerError('Nothing')
+        except stats_httpd.HttpServerError as err:
+            self.assertEqual(str(err), 'Nothing')
+
+class TestHttpServer(unittest.TestCase):
+    """Tests for HttpServer class"""
+
+    def setUp(self):
+        self.verbose = True
+        self.stats_httpd = stats_httpd.StatsHttpd(self.verbose)
+        self.stats_httpd.cc_session.verbose = False
+        for ht in self.stats_httpd.httpd:
+            self.assertTrue(ht.server_address in self.stats_httpd.http_addrs)
+            self.assertEqual(self.ht.verbose, self.verbose)
+            self.assertEqual(self.ht.xml_handler, self.stats_httpd.xml_handler)
+            self.assertEqual(self.ht.xsd_handler, self.stats_httpd.xsd_handler)
+            self.assertEqual(self.ht.xsl_handler, self.stats_httpd.xsl_handler)
+            self.assertEqual(self.ht.log_writer, self.stats_httpd.write_log)
+            self.assertTrue(isinstance(self.ht._handler, stats_httpd.HttpHandler))
+            self.assertTrue(isinstance(self.ht.socket, socket.socket))
+
+class TestStatsHttpdError(unittest.TestCase):
+    """Tests for StatsHttpdError exception"""
+
+    def test_raises(self):
+        try:
+            raise stats_httpd.StatsHttpdError('Nothing')
+        except stats_httpd.StatsHttpdError as err:
+            self.assertEqual(str(err), 'Nothing')
+
+class TestStatsHttpd(unittest.TestCase):
+    """Tests for StatsHttpd class"""
+
+    def setUp(self):
+        self.verbose = True
+        socket._CLOSED = False
+        socket.has_ipv6 = True
+        self.stats_httpd = stats_httpd.StatsHttpd(self.verbose)
+        self.stats_httpd.cc_session.verbose = False
+
+    def tearDown(self):
+        self.stats_httpd.stop()
+
+    def test_init(self):
+        self.assertTrue(self.stats_httpd.verbose)
+        self.assertFalse(self.stats_httpd.mccs.get_socket()._closed)
+        self.assertEqual(self.stats_httpd.mccs.get_socket().fileno(),
+                         id(self.stats_httpd.mccs.get_socket()))
+        for ht in self.stats_httpd.httpd:
+            self.assertFalse(ht.socket._closed)
+            self.assertEqual(ht.socket.fileno(), id(ht.socket))
+        socket._CLOSED = True
+        self.assertRaises(isc.cc.session.SessionError,
+                          stats_httpd.StatsHttpd)
+        socket._CLOSED = False
+
+    def test_mccs(self):
+        self.stats_httpd.open_mccs()
+        self.assertTrue(
+            isinstance(self.stats_httpd.mccs.get_socket(), socket.socket))
+        self.assertTrue(
+            isinstance(self.stats_httpd.cc_session, isc.cc.session.Session))
+        self.assertTrue(
+            isinstance(self.stats_httpd.stats_module_spec, isc.config.ModuleSpec))
+        for cfg in self.stats_httpd.stats_config_spec:
+            self.assertTrue('item_name' in cfg)
+            self.assertTrue(cfg['item_name'] in DUMMY_DATA)
+        self.assertTrue(len(self.stats_httpd.stats_config_spec), len(DUMMY_DATA))
+
+    def test_load_config(self):
+        self.stats_httpd.load_config()
+        self.assertTrue(('127.0.0.1', 8000) in set(self.stats_httpd.http_addrs))
+
+    def test_httpd(self):
+        # dual stack (addresses is ipv4 and ipv6)
+        socket.has_ipv6 = True
+        self.assertTrue(('127.0.0.1', 8000) in set(self.stats_httpd.http_addrs))
+        self.stats_httpd.http_addrs = [ ('::1', 8000), ('127.0.0.1', 8000) ]
+        self.assertTrue(
+            stats_httpd.HttpServer.address_family in set([socket.AF_INET, socket.AF_INET6]))
+        self.stats_httpd.open_httpd()
+        for ht in self.stats_httpd.httpd:
+            self.assertTrue(isinstance(ht.socket, socket.socket))
+        self.stats_httpd.close_httpd()
+
+        # dual stack (address is ipv6)
+        socket.has_ipv6 = True
+        self.stats_httpd.http_addrs = [ ('::1', 8000) ]
+        self.stats_httpd.open_httpd()
+        for ht in self.stats_httpd.httpd:
+            self.assertTrue(isinstance(ht.socket, socket.socket))
+        self.stats_httpd.close_httpd()
+
+        # dual stack (address is ipv4)
+        socket.has_ipv6 = True
+        self.stats_httpd.http_addrs = [ ('127.0.0.1', 8000) ]
+        self.stats_httpd.open_httpd()
+        for ht in self.stats_httpd.httpd:
+            self.assertTrue(isinstance(ht.socket, socket.socket))
+        self.stats_httpd.close_httpd()
+
+        # only-ipv4 single stack
+        socket.has_ipv6 = False
+        self.stats_httpd.http_addrs = [ ('127.0.0.1', 8000) ]
+        self.stats_httpd.open_httpd()
+        for ht in self.stats_httpd.httpd:
+            self.assertTrue(isinstance(ht.socket, socket.socket))
+        self.stats_httpd.close_httpd()
+
+        # only-ipv4 single stack (force set ipv6 )
+        socket.has_ipv6 = False
+        self.stats_httpd.http_addrs = [ ('::1', 8000) ]
+        self.assertRaises(stats_httpd.HttpServerError,
+            self.stats_httpd.open_httpd)
+
+        # hostname
+        self.stats_httpd.http_addrs = [ ('localhost', 8000) ]
+        self.stats_httpd.open_httpd()
+        for ht in self.stats_httpd.httpd:
+            self.assertTrue(isinstance(ht.socket, socket.socket))
+        self.stats_httpd.close_httpd()
+
+        self.stats_httpd.http_addrs = [ ('my.host.domain', 8000) ]
+        self.stats_httpd.open_httpd()
+        for ht in self.stats_httpd.httpd:
+            self.assertTrue(isinstance(ht.socket, socket.socket))
+        self.stats_httpd.close_httpd()
+
+        # over flow of port number
+        self.stats_httpd.http_addrs = [ ('', 80000) ]
+        self.assertRaises(stats_httpd.HttpServerError, self.stats_httpd.open_httpd)
+        # negative
+        self.stats_httpd.http_addrs = [ ('', -8000) ]
+        self.assertRaises(stats_httpd.HttpServerError, self.stats_httpd.open_httpd)
+        # alphabet
+        self.stats_httpd.http_addrs = [ ('', 'ABCDE') ]
+        self.assertRaises(stats_httpd.HttpServerError, self.stats_httpd.open_httpd)
+
+    def test_start(self):
+        self.stats_httpd.cc_session.group_sendmsg(
+            { 'command': [ "shutdown" ] }, "StatsHttpd")
+        self.stats_httpd.start()
+        self.stats_httpd = stats_httpd.StatsHttpd(self.verbose)
+        self.stats_httpd.cc_session.verbose = False
+        self.assertRaises(
+            select.error, self.stats_httpd.start)
+
+    def test_stop(self):
+        # success case
+        socket._CLOSED = False
+        self.stats_httpd.stop()
+        self.assertFalse(self.stats_httpd.running)
+        self.assertIsNone(self.stats_httpd.mccs)
+        for ht in self.stats_httpd.httpd:
+            self.assertTrue(ht.socket._closed)
+        self.assertTrue(self.stats_httpd.cc_session._socket._closed)
+        # failure case
+        self.stats_httpd.cc_session._socket._closed = False
+        self.stats_httpd.open_mccs()
+        self.stats_httpd.cc_session._socket._closed = True
+        self.stats_httpd.stop() # No excetion raises
+        self.stats_httpd.cc_session._socket._closed = False
+
+    def test_open_template(self):
+        self.assertRaises(
+            IOError,
+            self.stats_httpd.open_template, '/path/to/foo/bar')
+
+    def test_commands(self):
+        self.assertEqual(self.stats_httpd.command_handler("status", None),
+                         isc.config.ccsession.create_answer(
+                0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")"))
+        self.stats_httpd.running = True
+        self.assertEqual(self.stats_httpd.command_handler("shutdown", None),
+                         isc.config.ccsession.create_answer(
+                0, "Stats Httpd is shutting down."))
+        self.assertFalse(self.stats_httpd.running)
+        self.assertEqual(
+            self.stats_httpd.command_handler("__UNKNOWN_COMMAND__", None),
+            isc.config.ccsession.create_answer(
+                1, "Unknown command: __UNKNOWN_COMMAND__"))
+
+    def test_config(self):
+        d = dict(_UNKNOWN_KEY_=None)
+        assert type(d) is dict
+        self.assertEqual(
+            self.stats_httpd.config_handler(d),
+            isc.config.ccsession.create_answer(
+                    1, "Unknown known config: _UNKNOWN_KEY_"))
+        self.assertEqual(
+            self.stats_httpd.config_handler(
+                        dict(listen_on=[dict(address="::2",port=8000)])),
+            isc.config.ccsession.create_answer(0))
+        self.assertEqual(
+            self.stats_httpd.config_handler(
+                        dict(listen_on=[dict(address="::1",port=80)])),
+            isc.config.ccsession.create_answer(0))
+        self.assertEqual(
+            self.stats_httpd.config_handler(
+                        dict(listen_on=[dict(address="1.2.3.4",port=54321)])),
+            isc.config.ccsession.create_answer(0))
+        (ret, arg) = isc.config.ccsession.parse_answer(
+            self.stats_httpd.config_handler(
+                dict(listen_on=[dict(address="1.2.3.4",port=543210)]))
+            )
+        self.assertEqual(ret, 1)
+
+    def test_no_buildpath(self):
+        """
+        test for no "B10_FROM_SOURCE"
+        """
+        if "B10_FROM_SOURCE" in os.environ:
+            tmppath = os.environ["B10_FROM_SOURCE"]
+            os.environ.pop("B10_FROM_SOURCE")
+            imp.reload(stats_httpd)
+            os.environ["B10_FROM_SOURCE"] = tmppath
+            imp.reload(stats_httpd)
+
+if __name__ == "__main__":
+    unittest.main()

+ 0 - 115
src/bin/stats/tests/b10-stats_stub_test.py

@@ -1,115 +0,0 @@
-# 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.
-
-__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()

+ 8 - 10
src/bin/stats/tests/b10-stats_test.py

@@ -1,4 +1,4 @@
-# Copyright (C) 2010  Internet Systems Consortium.
+# Copyright (C) 2010, 2011  Internet Systems Consortium.
 #
 #
 # Permission to use, copy, modify, and distribute this software for any
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
 # purpose with or without fee is hereby granted, provided that the above
@@ -13,8 +13,6 @@
 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
 
-__version__ = "$Revision$"
-
 #
 #
 # Tests for the stats module
 # Tests for the stats module
 #
 #
@@ -530,9 +528,9 @@ class TestStats2(unittest.TestCase):
         Test for specfile
         Test for specfile
         
         
         """
         """
-        if "B10_FROM_BUILD" in os.environ:
+        if "B10_FROM_SOURCE" in os.environ:
             self.assertEqual(stats.SPECFILE_LOCATION,
             self.assertEqual(stats.SPECFILE_LOCATION,
-                             os.environ["B10_FROM_BUILD"] + "/src/bin/stats/stats.spec")
+                             os.environ["B10_FROM_SOURCE"] + os.sep + "stats.spec")
         imp.reload(stats)
         imp.reload(stats)
         # change path of SPECFILE_LOCATION
         # change path of SPECFILE_LOCATION
         stats.SPECFILE_LOCATION = TEST_SPECFILE_LOCATION
         stats.SPECFILE_LOCATION = TEST_SPECFILE_LOCATION
@@ -626,13 +624,13 @@ class TestStats2(unittest.TestCase):
 
 
     def test_osenv(self):
     def test_osenv(self):
         """
         """
-        test for not having environ "B10_FROM_BUILD"
+        test for not having environ "B10_FROM_SOURCE"
         """
         """
-        if "B10_FROM_BUILD" in os.environ:
-            path = os.environ["B10_FROM_BUILD"]
-            os.environ.pop("B10_FROM_BUILD")
+        if "B10_FROM_SOURCE" in os.environ:
+            path = os.environ["B10_FROM_SOURCE"]
+            os.environ.pop("B10_FROM_SOURCE")
             imp.reload(stats)
             imp.reload(stats)
-            os.environ["B10_FROM_BUILD"] = path
+            os.environ["B10_FROM_SOURCE"] = path
             imp.reload(stats)
             imp.reload(stats)
 
 
 def result_ok(*args):
 def result_ok(*args):

+ 2 - 0
src/bin/stats/tests/http/Makefile.am

@@ -0,0 +1,2 @@
+EXTRA_DIST = __init__.py server.py
+CLEANFILES = __init__.pyc server.pyc

+ 0 - 0
src/bin/stats/tests/http/__init__.py


+ 90 - 0
src/bin/stats/tests/http/server.py

@@ -0,0 +1,90 @@
+# Copyright (C) 2011  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import socket
+
+class DummyHttpResponse:
+    def __init__(self, path):
+        self.path = path
+        self.headers={}
+        self.log = ""
+
+    def _write_log(self, msg):
+        assert type(msg) is str
+        self.log = self.log + msg
+
+class HTTPServer:
+    """
+    This module is a mock-up class of http.server.HTTPServer
+    """
+    address_family = socket.AF_INET
+    def __init__(self, server_class, handler_class):
+        self.socket = socket.socket(self.address_family)
+        self.server_class = server_class
+        self.socket.bind(self.server_class)
+        self._handler = handler_class(None, None, self)
+
+    def handle_request(self):
+        pass
+
+    def server_close(self):
+        self.socket.close()
+
+class BaseHTTPRequestHandler:
+    """
+    This module is a mock-up class of http.server.BaseHTTPRequestHandler
+    """
+
+    def __init__(self, request, client_address, server):
+        self.path = "/path/to"
+        self.headers = {}
+        self.server = server
+        self.response = DummyHttpResponse(path=self.path)
+        self.response.write = self._write
+        self.wfile = self.response
+
+    def send_response(self, code=0):
+        if self.path != self.response.path:
+            self.response = DummyHttpResponse(path=self.path)
+        self.response.code = code
+
+    def send_header(self, key, value):
+        if self.path != self.response.path:
+            self.response = DummyHttpResponse(path=self.path)
+        self.response.headers[key] = value
+
+    def end_headers(self):
+        if self.path != self.response.path:
+            self.response = DummyHttpResponse(path=self.path)
+        self.response.wrote_headers = True
+
+    def send_error(self, code, message=None):
+        if self.path != self.response.path:
+            self.response = DummyHttpResponse(path=self.path)
+        self.response.code = code
+        self.response.body = message
+
+    def address_string(self):
+        return 'dummyhost'
+
+    def log_date_time_string(self):
+        return '[DD/MM/YYYY HH:MI:SS]'
+
+    def _write(self, obj):
+        if self.path != self.response.path:
+            self.response = DummyHttpResponse(path=self.path)
+        assert type(obj) is bytes
+        self.response.body = obj.decode()
+

+ 26 - 10
src/bin/stats/tests/isc/cc/session.py

@@ -1,4 +1,4 @@
-# Copyright (C) 2010  Internet Systems Consortium.
+# Copyright (C) 2010,2011  Internet Systems Consortium.
 #
 #
 # Permission to use, copy, modify, and distribute this software for any
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
 # purpose with or without fee is hereby granted, provided that the above
@@ -15,9 +15,8 @@
 
 
 # This module is a mock-up class of isc.cc.session
 # This module is a mock-up class of isc.cc.session
 
 
-__version__ = "$Revision$"
-
 import sys
 import sys
+import socket
 
 
 # set a dummy lname
 # set a dummy lname
 _TEST_LNAME = '123abc@xxxx'
 _TEST_LNAME = '123abc@xxxx'
@@ -33,12 +32,18 @@ class Queue():
 class SessionError(Exception):
 class SessionError(Exception):
     pass
     pass
 
 
+class SessionTimeout(Exception):
+    pass
+
 class Session:
 class Session:
     def __init__(self, socket_file=None, verbose=False):
     def __init__(self, socket_file=None, verbose=False):
         self._lname = _TEST_LNAME
         self._lname = _TEST_LNAME
         self.message_queue = []
         self.message_queue = []
         self.old_message_queue = []
         self.old_message_queue = []
-        self._socket = True
+        try:
+            self._socket = socket.socket()
+        except socket.error as se:
+            raise SessionError(se)
         self.verbose = verbose
         self.verbose = verbose
 
 
     @property
     @property
@@ -46,13 +51,17 @@ class Session:
         return self._lname
         return self._lname
 
 
     def close(self):
     def close(self):
-        self._socket = False
+        self._socket.close()
+
+    def _clear_ques(self):
+        while len(self.message_queue) > 0:
+            self.dequeue()
 
 
     def _next_sequence(self, que=None):
     def _next_sequence(self, que=None):
         return len(self.message_queue)
         return len(self.message_queue)
 
 
     def enqueue(self, msg=None, env={}):
     def enqueue(self, msg=None, env={}):
-        if not self._socket:
+        if self._socket._closed:
             raise SessionError("Session has been closed.")
             raise SessionError("Session has been closed.")
         seq = self._next_sequence()
         seq = self._next_sequence()
         env.update({"seq": 0}) # fixed here
         env.update({"seq": 0}) # fixed here
@@ -63,11 +72,11 @@ class Session:
         return seq
         return seq
 
 
     def dequeue(self, seq=0):
     def dequeue(self, seq=0):
-        if not self._socket:
+        if self._socket._closed:
             raise SessionError("Session has been closed.")
             raise SessionError("Session has been closed.")
         que = None
         que = None
         try:
         try:
-            que = self.message_queue.pop(seq)
+            que = self.message_queue.pop(0) # always pop at index 0
             self.old_message_queue.append(que)
             self.old_message_queue.append(que)
         except IndexError:
         except IndexError:
             que = Queue()
             que = Queue()
@@ -76,7 +85,7 @@ class Session:
         return que
         return que
 
 
     def get_queue(self, seq=None):
     def get_queue(self, seq=None):
-        if not self._socket:
+        if self._socket._closed:
             raise SessionError("Session has been closed.")
             raise SessionError("Session has been closed.")
         if seq is None:
         if seq is None:
             seq = len(self.message_queue) - 1
             seq = len(self.message_queue) - 1
@@ -112,7 +121,7 @@ class Session:
                 "reply": routing["seq"] })
                 "reply": routing["seq"] })
 
 
     def get_message(self, group, to='*'):
     def get_message(self, group, to='*'):
-        if not self._socket:
+        if self._socket._closed:
             raise SessionError("Session has been closed.")
             raise SessionError("Session has been closed.")
         que = Queue()
         que = Queue()
         for q in self.message_queue:
         for q in self.message_queue:
@@ -124,3 +133,10 @@ class Session:
             sys.stdout.write("[Session] get_message: " + str(que.dump()) + "\n")
             sys.stdout.write("[Session] get_message: " + str(que.dump()) + "\n")
         return q.msg
         return q.msg
 
 
+    def group_subscribe(self, group, instance = "*"):
+        if self._socket._closed:
+            raise SessionError("Session has been closed.")
+
+    def group_unsubscribe(self, group, instance = "*"):
+        if self._socket._closed:
+            raise SessionError("Session has been closed.")

+ 61 - 6
src/bin/stats/tests/isc/config/ccsession.py

@@ -1,4 +1,4 @@
-# Copyright (C) 2010  Internet Systems Consortium.
+# Copyright (C) 2010,2011  Internet Systems Consortium.
 #
 #
 # Permission to use, copy, modify, and distribute this software for any
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
 # purpose with or without fee is hereby granted, provided that the above
@@ -15,14 +15,15 @@
 
 
 # This module is a mock-up class of isc.cc.session
 # This module is a mock-up class of isc.cc.session
 
 
-__version__ = "$Revision$"
-
 import json
 import json
+import os
 from isc.cc.session import Session
 from isc.cc.session import Session
 
 
 COMMAND_CONFIG_UPDATE = "config_update"
 COMMAND_CONFIG_UPDATE = "config_update"
 
 
 def parse_answer(msg):
 def parse_answer(msg):
+    assert type(msg) is dict
+    assert 'result' in msg
     try:
     try:
         return msg['result'][0], msg['result'][1]
         return msg['result'][0], msg['result'][1]
     except IndexError:
     except IndexError:
@@ -35,6 +36,8 @@ def create_answer(rcode, arg = None):
         return { 'result': [ rcode, arg ] }
         return { 'result': [ rcode, arg ] }
 
 
 def parse_command(msg):
 def parse_command(msg):
+    assert type(msg) is dict
+    assert 'command' in msg
     try:
     try:
         return msg['command'][0], msg['command'][1]
         return msg['command'][0], msg['command'][1]
     except IndexError:
     except IndexError:
@@ -47,9 +50,21 @@ def create_command(command_name, params = None):
         return {"command": [command_name, params]}
         return {"command": [command_name, params]}
 
 
 def module_spec_from_file(spec_file, check = True):
 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)
+    try:
+        file = open(spec_file)
+        json_str = file.read()
+        module_spec = json.loads(json_str)
+        file.close()
+        return ModuleSpec(module_spec['module_spec'], check)
+    except IOError as ioe:
+        raise ModuleSpecError("JSON read error: " + str(ioe))
+    except ValueError as ve:
+        raise ModuleSpecError("JSON parse error: " + str(ve))
+    except KeyError as err:
+        raise ModuleSpecError("Data definition has no module_spec element")
+
+class ModuleSpecError(Exception):
+    pass
 
 
 class ModuleSpec:
 class ModuleSpec:
     def __init__(self, module_spec, check = True):
     def __init__(self, module_spec, check = True):
@@ -67,10 +82,46 @@ class ModuleSpec:
 class ModuleCCSessionError(Exception):
 class ModuleCCSessionError(Exception):
     pass
     pass
 
 
+class DataNotFoundError(Exception):
+    pass
+
 class ConfigData:
 class ConfigData:
     def __init__(self, specification):
     def __init__(self, specification):
         self.specification = specification
         self.specification = specification
 
 
+    def get_value(self, identifier):
+        """Returns a tuple where the first item is the value at the
+           given identifier, and the second item is a bool which is
+           true if the value is an unset default. Raises an
+           DataNotFoundError if the identifier is bad"""
+        def _get_value(config_map):
+                if 'item_default' in config_map:
+                    return config_map['item_default'], False
+                elif 'item_type' in config_map:
+                    if config_map['item_type'] == 'boolean':
+                        return bool(), True
+                    elif config_map['item_type'] == 'string':
+                        return str(), True
+                    elif config_map['item_type'] in set(['number', 'integer']):
+                        return int(), True
+                    elif config_map['item_type'] in set(['float', 'double', 'real']):
+                        return float(), True
+                    elif config_map['item_type'] in set(['list', 'array']):
+                        return [ _get_value(conf)
+                                 for conf in spec['list_item_spec'] ], True
+                    elif config_map['item_type'] in set(['map', 'object']):
+                        return dict(
+                            [ (conf['item_name'], _get_value(conf))
+                              for conf in config_map['map_item_spec'] ]), True
+                return None, True
+        for config_map in self.get_module_spec().get_config_spec():
+            if config_map['item_name'] == identifier:
+                return _get_value(config_map)
+        raise DataNotFoundError("item_name %s is not found in the specfile" % identifier)
+
+    def get_module_spec(self):
+        return self.specification
+
 class ModuleCCSession(ConfigData):
 class ModuleCCSession(ConfigData):
     def __init__(self, spec_file_name, config_handler, command_handler, cc_session = None):
     def __init__(self, spec_file_name, config_handler, command_handler, cc_session = None):
         module_spec = module_spec_from_file(spec_file_name)
         module_spec = module_spec_from_file(spec_file_name)
@@ -111,3 +162,7 @@ class ModuleCCSession(ConfigData):
 
 
     def get_module_spec(self):
     def get_module_spec(self):
         return self.specification
         return self.specification
+
+    def get_socket(self):
+        return self._session._socket
+

+ 38 - 0
src/bin/stats/tests/select.py

@@ -0,0 +1,38 @@
+# Copyright (C) 2011  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import socket
+import errno
+
+class error(Exception):
+    pass
+
+def select(rlst, wlst, xlst, timeout):
+    """
+    This module is a mock-up function of select.select
+    """
+    if type(timeout) != int and type(timeout) != float:
+            raise TypeError("Error: %s must be integer or float"
+                            % timeout.__class__.__name__)
+    for s in rlst + wlst + xlst:
+        if type(s) != socket.socket:
+            raise TypeError("Error: %s must be a dummy socket"
+                            % s.__class__.__name__)
+        s._called = s._called + 1
+        if s._called > 3:
+            raise error("Something is happened!")
+        elif s._called > 2:
+            raise error(errno.EINTR)
+    return (rlst, wlst, xlst)

+ 66 - 0
src/bin/stats/tests/socket.py

@@ -0,0 +1,66 @@
+# Copyright (C) 2011  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""
+This module is a mock-up classes of socket module
+"""
+
+import re
+
+AF_INET = 'AF_INET'
+AF_INET6 = 'AF_INET6'
+_ADDRFAMILY = AF_INET
+has_ipv6 = True
+_CLOSED = False
+
+class gaierror(Exception):
+    pass
+
+class error(Exception):
+    pass
+
+class socket:
+
+    def __init__(self, family=None):
+        if family is None:
+            self.address_family = _ADDRFAMILY
+        else:
+            self.address_family = family
+        self._closed = _CLOSED
+        if self._closed:
+            raise error('socket is already closed!')
+        self._called = 0
+
+    def close(self):
+        self._closed = True
+
+    def fileno(self):
+        return id(self)
+
+    def bind(self, server_class):
+        (self.server_address, self.server_port) = server_class
+        if self.address_family not in set([AF_INET, AF_INET6]):
+            raise error("Address family not supported by protocol: %s" % self.address_family)
+        if self.address_family == AF_INET6 and not has_ipv6:
+            raise error("Address family not supported in this machine: %s has_ipv6: %s"
+                        % (self.address_family, str(has_ipv6)))
+        if self.address_family == AF_INET and re.search(':', self.server_address) is not None:
+            raise gaierror("Address family for hostname not supported : %s %s" % (self.server_address, self.address_family))
+        if self.address_family == AF_INET6 and re.search(':', self.server_address) is None:
+            raise error("Cannot assign requested address : %s" % str(self.server_address))
+        if type(self.server_port) is not int:
+            raise TypeError("an integer is required: %s" % str(self.server_port))
+        if self.server_port < 0 or self.server_port > 65535:
+            raise OverflowError("port number must be 0-65535.: %s" % str(self.server_port))

+ 4 - 4
src/bin/stats/tests/stats_test.in

@@ -1,6 +1,6 @@
 #! /bin/sh
 #! /bin/sh
 
 
-# Copyright (C) 2010  Internet Systems Consortium.
+# Copyright (C) 2010, 2011  Internet Systems Consortium.
 #
 #
 # Permission to use, copy, modify, and distribute this software for any
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
 # purpose with or without fee is hereby granted, provided that the above
@@ -21,11 +21,11 @@ export PYTHON_EXEC
 PYTHONPATH=@abs_top_builddir@/src/lib/python:@abs_top_srcdir@/src/bin/stats:@abs_top_srcdir@/src/bin/stats/tests
 PYTHONPATH=@abs_top_builddir@/src/lib/python:@abs_top_srcdir@/src/bin/stats:@abs_top_srcdir@/src/bin/stats/tests
 export PYTHONPATH
 export PYTHONPATH
 
 
-B10_FROM_BUILD=@abs_top_builddir@
-export B10_FROM_BUILD
+B10_FROM_SOURCE=@abs_top_srcdir@/src/bin/stats
+export B10_FROM_SOURCE
 
 
 TEST_PATH=@abs_top_srcdir@/src/bin/stats/tests
 TEST_PATH=@abs_top_srcdir@/src/bin/stats/tests
 
 
 cd ${TEST_PATH}
 cd ${TEST_PATH}
 ${PYTHON_EXEC} -O b10-stats_test.py $*
 ${PYTHON_EXEC} -O b10-stats_test.py $*
-${PYTHON_EXEC} -O b10-stats_stub_test.py $*
+${PYTHON_EXEC} -O b10-stats-httpd_test.py $*