Browse Source

[master] Merge branch 'trac547-summarized'

Conflicts:
	ChangeLog
	src/bin/stats/tests/b10-stats_test.py
Naoki Kambe 14 years ago
parent
commit
1cbd519192

+ 11 - 0
ChangeLog

@@ -1,3 +1,14 @@
+225.	[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)
+
 224.	[bug]		jinmei
 	b10-auth, src/lib/datasrc: inconsistency between the hot spot
 	cache and actual data source could cause a crash while query

+ 9 - 4
configure.ac

@@ -640,6 +640,7 @@ AC_CONFIG_FILES([Makefile
                  src/bin/stats/tests/isc/config/Makefile
                  src/bin/stats/tests/isc/util/Makefile
                  src/bin/stats/tests/testdata/Makefile
+                 src/bin/stats/tests/http/Makefile
                  src/bin/usermgr/Makefile
                  src/bin/tests/Makefile
                  src/lib/Makefile
@@ -729,10 +730,14 @@ AC_OUTPUT([doc/version.ent
            src/bin/zonemgr/tests/zonemgr_test
            src/bin/zonemgr/run_b10-zonemgr.sh
            src/bin/stats/stats.py
-           src/bin/stats/stats_stub.py
-           src/bin/stats/stats.spec.pre
+           src/bin/stats/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_stub.sh
+           src/bin/stats/run_b10-stats-httpd.sh
            src/bin/stats/tests/stats_test
            src/bin/bind10/bind10.py
            src/bin/bind10/run_bind10.sh
@@ -774,7 +779,7 @@ AC_OUTPUT([doc/version.ent
            chmod +x src/bin/zonemgr/run_b10-zonemgr.sh
            chmod +x src/bin/stats/tests/stats_test
            chmod +x src/bin/stats/run_b10-stats.sh
-           chmod +x src/bin/stats/run_b10-stats_stub.sh
+           chmod +x src/bin/stats/run_b10-stats-httpd.sh
            chmod +x src/bin/bind10/run_bind10.sh
            chmod +x src/bin/cmdctl/tests/cmdctl_test
            chmod +x src/bin/xfrin/tests/xfrin_test

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

@@ -546,6 +546,9 @@ class BoB:
     def start_stats(self, 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):
         """
             Starts the command control process
@@ -595,6 +598,7 @@ class BoB:
 
         # ... and finally start the remaining processes
         self.start_stats(c_channel_env)
+        self.start_stats_httpd(c_channel_env)
         self.start_cmdctl(c_channel_env)
     
     def startup(self):
@@ -644,6 +648,7 @@ class BoB:
         self.cc_session.group_sendmsg(cmd, "Xfrin", "Xfrin")
         self.cc_session.group_sendmsg(cmd, "Zonemgr", "Zonemgr")
         self.cc_session.group_sendmsg(cmd, "Stats", "Stats")
+        self.cc_session.group_sendmsg(cmd, "StatsHttpd", "StatsHttpd")
 
     def stop_process(self, process, recipient):
         """

+ 17 - 4
src/bin/bind10/tests/bind10_test.py.in

@@ -183,6 +183,7 @@ class MockBob(BoB):
         self.xfrin = False
         self.zonemgr = False
         self.stats = False
+        self.stats_httpd = False
         self.cmdctl = False
         self.c_channel_env = {}
         self.processes = { }
@@ -236,10 +237,15 @@ class MockBob(BoB):
         self.processes[10] = ProcessInfo('b10-stats', ['/bin/false'])
         self.processes[10].pid = 10
 
+    def start_stats_httpd(self, c_channel_env):
+        self.stats_httpd = True
+        self.processes[11] = ProcessInfo('b10-stats-httpd', ['/bin/false'])
+        self.processes[11].pid = 11
+
     def start_cmdctl(self, c_channel_env):
         self.cmdctl = True
-        self.processes[11] = ProcessInfo('b10-cmdctl', ['/bin/false'])
-        self.processes[11].pid = 11
+        self.processes[12] = ProcessInfo('b10-cmdctl', ['/bin/false'])
+        self.processes[12].pid = 12
 
     # We don't really use all of these stop_ methods. But it might turn out
     # someone would add some stop_ method to BoB and we want that one overriden
@@ -289,9 +295,14 @@ class MockBob(BoB):
             del self.processes[10]
         self.stats = False
 
+    def stop_stats_httpd(self):
+        if self.stats_httpd:
+            del self.processes[11]
+        self.stats_httpd = False
+
     def stop_cmdctl(self):
         if self.cmdctl:
-            del self.processes[11]
+            del self.processes[12]
         self.cmdctl = False
 
 class TestStartStopProcessesBob(unittest.TestCase):
@@ -316,6 +327,7 @@ class TestStartStopProcessesBob(unittest.TestCase):
         self.assertEqual(bob.xfrin, auth)
         self.assertEqual(bob.zonemgr, auth)
         self.assertEqual(bob.stats, core)
+        self.assertEqual(bob.stats_httpd, core)
         self.assertEqual(bob.cmdctl, core)
 
     def check_preconditions(self, bob):
@@ -544,7 +556,8 @@ class TestBossCmd(unittest.TestCase):
                      [8, 'b10-xfrin'], 
                      [9, 'b10-zonemgr'],
                      [10, 'b10-stats'], 
-                     [11, 'b10-cmdctl']]
+                     [11, 'b10-stats-httpd'], 
+                     [12, 'b10-cmdctl']]
         self.assertEqual(answer, {'result': [0, processes]})
 
 class TestParseArgs(unittest.TestCase):

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

@@ -2,35 +2,36 @@ SUBDIRS = tests
 
 pkglibexecdir = $(libexecdir)/@PACKAGE@
 
-pkglibexec_SCRIPTS = b10-stats
-noinst_SCRIPTS = b10-stats_stub
+pkglibexec_SCRIPTS = b10-stats b10-stats-httpd
 
 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
 
 b10-stats.8: b10-stats.xml
 	xsltproc --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-stats.xml
 
-endif
+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
 b10-stats: stats.py
 	$(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" \
-	       -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" \
 	       -e "s|.*#@@REMOVED@@$$||"  stats.py >$@
 	chmod a+x $@
 
-b10-stats_stub: stats_stub.py stats.py
-	$(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" \
-	       -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" stats_stub.py >$@
+b10-stats-httpd: stats_httpd.py
+	$(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" stats_httpd.py >$@
 	chmod a+x $@

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


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

@@ -0,0 +1,215 @@
+<!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 eventually
+      exited by it.  The server is intended to be server requests by HTTP
+      clients like web browsers and third-party modules. When the server is
+      asked, it requests BIND 10 statistics data from
+      <command>b10-stats</command>, and it sends the data back 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. There is different URL for each document. But please note that
+      you would be redirected to the URL of XML document if you request the URL
+      of the root document. For example, you would be redirected to
+      http://127.0.0.1:8000/bind10/statistics/xml if you request
+      http://127.0.0.1:8000/. 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 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 argument is as follow:</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>
+      <!--TODO: The filename should be computed from prefix-->
+      &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>
+      <!--TODO: The filename should be computed from prefix-->
+      &mdash; the template file of XML document.
+    </para>
+    <para>
+      <filename>/usr/local/share/bind10-devel/stats-httpd-xsd.tpl</filename>
+      <!--TODO: The filename should be computed from prefix-->
+      &mdash; the template file of XSD document.
+    </para>
+    <para>
+      <filename>/usr/local/share/bind10-devel/stats-httpd-xsl.tpl</filename>
+      <!--TODO: The filename should be computed from prefix-->
+      &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 consists 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. You can change the settings
+	    through <refentrytitle>bindctl</refentrytitle><manvolnum>8</manvolnum>.
+	  </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+    <para>
+      The 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.
+	  </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
 
-# Copyright (C) 2010  Internet Systems Consortium.
+# 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
@@ -21,13 +21,13 @@ export PYTHON_EXEC
 PYTHONPATH=@abs_top_builddir@/src/lib/python
 export PYTHONPATH
 
-B10_FROM_BUILD=@abs_top_srcdir@
-export B10_FROM_BUILD
-
 BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
 export BIND10_MSGQ_SOCKET_FILE
 
 STATS_PATH=@abs_top_builddir@/src/bin/stats
 
+B10_FROM_SOURCE=@abs_top_srcdir@/src/bin/stats
+export B10_FROM_SOURCE
+
 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
 
-# Copyright (C) 2010  Internet Systems Consortium.
+# Copyright (C) 2010, 2011  Internet Systems Consortium.
 #
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
@@ -24,8 +24,8 @@ export PYTHONPATH
 BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
 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
 

+ 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": []
+      }
+    ]
+  }
+}

+ 5 - 4
src/bin/stats/stats.py.in

@@ -35,15 +35,16 @@ if __name__ == 'stats':					#@@REMOVED@@
 import isc.util.process
 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
 # 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:
     PREFIX = "@prefix@"
     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):
     """

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

@@ -17,7 +17,7 @@
         "item_type": "string",
         "item_optional": false,
         "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_format": "date-time"
       },

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

@@ -0,0 +1,484 @@
+#!@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.
+
+"""
+A standalone HTTP server for HTTP/XML interface of statistics in BIND 10
+
+"""
+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)])
+
+# 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):
+        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 self.path == '/' and 'Host' in self.headers.keys():
+                    # redirect to XML URL only when requested with '/'
+                    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"""
+        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()
+
+    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")
+        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:
+            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()
+                        ])
+                )
+        # set addresses and ports for HTTP
+        self.http_addrs = [ (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"""
+        for addr in self.http_addrs:
+            self.httpd.append(self._open_httpd(addr))
+
+    def _open_httpd(self, server_address, address_family=None):
+        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)
+        return httpd
+
+    def close_httpd(self):
+        """Closes sockets for HTTP"""
+        if len(self.httpd) == 0:
+            return
+        for ht in self.httpd:
+            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:
+                # select.error exception is caught only in the case of
+                # EINTR, or in other cases it is just thrown.
+                if err.args[0] == errno.EINTR:
+                    (rfd, wfd, xfd) = ([], [], [])
+                else:
+                    raise
+            # FIXME: This module can handle only one request at a
+            # time. If someone sends only part of the request, we block
+            # waiting for it until we time out.
+            # But it isn't so big issue for administration purposes.
+            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."""
+        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)
+        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))
+        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):
+        """It opens a template file, and it loads all lines to a
+        string variable and returns string. Template object includes
+        the variable. Limitation of a file size isn't needed there."""
+        lines = "".join(
+            open(file_name, 'r').readlines())
+        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.exit("[b10-stats-httpd] Error parsing options")
+    except isc.cc.session.SessionError as se:
+        sys.exit("[b10-stats-httpd] Error creating module, "
+                 + "is the command channel daemon running?")
+    except HttpServerError as hse:
+        sys.exit("[b10-stats-httpd] %s" % hse)
+    except KeyboardInterrupt as kie:
+        sys.exit("[b10-stats-httpd] Interrupted, exiting")

+ 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@
-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 fake_socket.py fake_select.py
+CLEANFILES = fake_time.pyc fake_socket.pyc fake_select.pyc
 
 # test using command-line arguments, so use check-local target instead of TESTS
 check-local:
@@ -14,6 +14,6 @@ endif
 	for pytest in $(PYTESTS) ; do \
 	echo Running test: $$pytest ; \
 	env PYTHONPATH=$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python:$(abs_top_builddir)/src/bin/stats:$(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 ; \
 	done

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

@@ -0,0 +1,444 @@
+# 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 fake_select
+import imp
+import sys
+import fake_socket
+
+import isc.cc
+
+import stats_httpd
+stats_httpd.socket = fake_socket
+stats_httpd.select = fake_select
+
+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')
+        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.assertTrue(handler.response.body.find(stats_httpd.XSD_NAMESPACE)>0)
+        self.assertTrue(handler.response.body.find(stats_httpd.XSD_URL_PATH)>0)
+        for (k, v) in DUMMY_DATA.items():
+            self.assertTrue(handler.response.body.find(str(k))>0)
+            self.assertTrue(handler.response.body.find(str(v))>0)
+
+        # 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.assertTrue(handler.response.body.find(stats_httpd.XSD_NAMESPACE)>0)
+        for (k, v) in DUMMY_DATA.items():
+            self.assertTrue(handler.response.body.find(str(k))>0)
+
+        # 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.assertTrue(handler.response.body.find(stats_httpd.XSD_NAMESPACE)>0)
+        for (k, v) in DUMMY_DATA.items():
+            self.assertTrue(handler.response.body.find(str(k))>0)
+
+        # 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_queues()
+
+        # 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_queues()
+
+    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 test_httpserver(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(ht.verbose, self.verbose)
+            self.assertEqual(ht.xml_handler, self.stats_httpd.xml_handler)
+            self.assertEqual(ht.xsd_handler, self.stats_httpd.xsd_handler)
+            self.assertEqual(ht.xsl_handler, self.stats_httpd.xsl_handler)
+            self.assertEqual(ht.log_writer, self.stats_httpd.write_log)
+            self.assertTrue(isinstance(ht._handler, stats_httpd.HttpHandler))
+            self.assertTrue(isinstance(ht.socket, fake_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
+        fake_socket._CLOSED = False
+        fake_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))
+        fake_socket._CLOSED = True
+        self.assertRaises(isc.cc.session.SessionError,
+                          stats_httpd.StatsHttpd)
+        fake_socket._CLOSED = False
+
+    def test_mccs(self):
+        self.stats_httpd.open_mccs()
+        self.assertTrue(
+            isinstance(self.stats_httpd.mccs.get_socket(), fake_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)
+        fake_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([fake_socket.AF_INET, fake_socket.AF_INET6]))
+        self.stats_httpd.open_httpd()
+        for ht in self.stats_httpd.httpd:
+            self.assertTrue(isinstance(ht.socket, fake_socket.socket))
+        self.stats_httpd.close_httpd()
+
+        # dual stack (address is ipv6)
+        fake_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, fake_socket.socket))
+        self.stats_httpd.close_httpd()
+
+        # dual stack (address is ipv4)
+        fake_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, fake_socket.socket))
+        self.stats_httpd.close_httpd()
+
+        # only-ipv4 single stack
+        fake_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, fake_socket.socket))
+        self.stats_httpd.close_httpd()
+
+        # only-ipv4 single stack (force set ipv6 )
+        fake_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, fake_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, fake_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(
+            fake_select.error, self.stats_httpd.start)
+
+    def test_stop(self):
+        # success case
+        fake_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):
+        # successful conditions
+        tmpl = self.stats_httpd.open_template(stats_httpd.XML_TEMPLATE_LOCATION)
+        self.assertTrue(isinstance(tmpl, string.Template))
+        opts = dict(
+            xml_string="<dummy></dummy>",
+            xsd_namespace="http://host/path/to/",
+            xsd_url_path="/path/to/",
+            xsl_url_path="/path/to/")
+        lines = tmpl.substitute(opts)
+        for n in opts:
+            self.assertTrue(lines.find(opts[n])>0)
+        tmpl = self.stats_httpd.open_template(stats_httpd.XSD_TEMPLATE_LOCATION)
+        self.assertTrue(isinstance(tmpl, string.Template))
+        opts = dict(
+            xsd_string="<dummy></dummy>",
+            xsd_namespace="http://host/path/to/")
+        lines = tmpl.substitute(opts)
+        for n in opts:
+            self.assertTrue(lines.find(opts[n])>0)
+        tmpl = self.stats_httpd.open_template(stats_httpd.XSL_TEMPLATE_LOCATION)
+        self.assertTrue(isinstance(tmpl, string.Template))
+        opts = dict(
+            xsl_string="<dummy></dummy>",
+            xsd_namespace="http://host/path/to/")
+        lines = tmpl.substitute(opts)
+        for n in opts:
+            self.assertTrue(lines.find(opts[n])>0)
+        # unsuccessful condition
+        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):
+        self.assertEqual(
+            self.stats_httpd.config_handler(dict(_UNKNOWN_KEY_=None)),
+            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.assertTrue("listen_on" in self.stats_httpd.config)
+        for addr in self.stats_httpd.config["listen_on"]:
+            self.assertTrue("address" in addr)
+            self.assertTrue("port" in addr)
+            self.assertTrue(addr["address"] == "::2")
+            self.assertTrue(addr["port"] == 8000)
+
+        self.assertEqual(
+            self.stats_httpd.config_handler(
+                        dict(listen_on=[dict(address="::1",port=80)])),
+            isc.config.ccsession.create_answer(0))
+        self.assertTrue("listen_on" in self.stats_httpd.config)
+        for addr in self.stats_httpd.config["listen_on"]:
+            self.assertTrue("address" in addr)
+            self.assertTrue("port" in addr)
+            self.assertTrue(addr["address"] == "::1")
+            self.assertTrue(addr["port"] == 80)
+
+        self.assertEqual(
+            self.stats_httpd.config_handler(
+                        dict(listen_on=[dict(address="1.2.3.4",port=54321)])),
+            isc.config.ccsession.create_answer(0))
+        self.assertTrue("listen_on" in self.stats_httpd.config)
+        for addr in self.stats_httpd.config["listen_on"]:
+            self.assertTrue("address" in addr)
+            self.assertTrue("port" in addr)
+            self.assertTrue(addr["address"] == "1.2.3.4")
+            self.assertTrue(addr["port"] == 54321)
+        (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_for_without_B10_FROM_SOURCE(self):
+        # just lets it go through the code without B10_FROM_SOURCE env
+        # variable
+        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)
+            stats_httpd.socket = fake_socket
+            stats_httpd.select = fake_select
+
+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 - 8
src/bin/stats/tests/b10-stats_test.py

@@ -1,4 +1,4 @@
-# Copyright (C) 2010,2011  Internet Systems Consortium.
+# Copyright (C) 2010, 2011  Internet Systems Consortium.
 #
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
@@ -535,9 +535,9 @@ class TestStats2(unittest.TestCase):
         Test for specfile
         
         """
-        if "B10_FROM_BUILD" in os.environ:
+        if "B10_FROM_SOURCE" in os.environ:
             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)
         # change path of SPECFILE_LOCATION
         stats.SPECFILE_LOCATION = TEST_SPECFILE_LOCATION
@@ -631,13 +631,13 @@ class TestStats2(unittest.TestCase):
 
     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)
-            os.environ["B10_FROM_BUILD"] = path
+            os.environ["B10_FROM_SOURCE"] = path
             imp.reload(stats)
 
 def result_ok(*args):

+ 43 - 0
src/bin/stats/tests/fake_select.py

@@ -0,0 +1,43 @@
+# 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.
+
+"""
+A mock-up module of select
+
+*** NOTE ***
+It is only for testing stats_httpd module and not reusable for
+external module.
+"""
+
+import fake_socket
+import errno
+
+class error(Exception):
+    pass
+
+def select(rlst, wlst, xlst, timeout):
+    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) != fake_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)

+ 70 - 0
src/bin/stats/tests/fake_socket.py

@@ -0,0 +1,70 @@
+# 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.
+
+"""
+A mock-up module of socket
+
+*** NOTE ***
+It is only for testing stats_httpd module and not reusable for
+external 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))

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

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

src/bin/stats/tests/isc/utils/__init__.py → src/bin/stats/tests/http/__init__.py


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

@@ -0,0 +1,96 @@
+# 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.
+
+"""
+A mock-up module of http.server
+
+*** NOTE ***
+It is only for testing stats_httpd module and not reusable for
+external module.
+"""
+
+import fake_socket
+
+class DummyHttpResponse:
+    def __init__(self, path):
+        self.path = path
+        self.headers={}
+        self.log = ""
+
+    def _write_log(self, msg):
+        self.log = self.log + msg
+
+class HTTPServer:
+    """
+    A mock-up class of http.server.HTTPServer
+    """
+    address_family = fake_socket.AF_INET
+    def __init__(self, server_class, handler_class):
+        self.socket = fake_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:
+    """
+    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)
+        self.response.body = obj.decode()
+

+ 34 - 12
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
 # purpose with or without fee is hereby granted, provided that the above
@@ -13,11 +13,16 @@
 # 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 class of isc.cc.session
+"""
+A mock-up module of isc.cc.session
 
-__version__ = "$Revision$"
+*** NOTE ***
+It is only for testing stats_httpd module and not reusable for
+external module.
+"""
 
 import sys
+import fake_socket
 
 # set a dummy lname
 _TEST_LNAME = '123abc@xxxx'
@@ -33,12 +38,18 @@ class Queue():
 class SessionError(Exception):
     pass
 
+class SessionTimeout(Exception):
+    pass
+
 class Session:
     def __init__(self, socket_file=None, verbose=False):
         self._lname = _TEST_LNAME
         self.message_queue = []
         self.old_message_queue = []
-        self._socket = True
+        try:
+            self._socket = fake_socket.socket()
+        except fake_socket.error as se:
+            raise SessionError(se)
         self.verbose = verbose
 
     @property
@@ -46,13 +57,17 @@ class Session:
         return self._lname
 
     def close(self):
-        self._socket = False
+        self._socket.close()
+
+    def _clear_queues(self):
+        while len(self.message_queue) > 0:
+            self.dequeue()
 
     def _next_sequence(self, que=None):
         return len(self.message_queue)
 
     def enqueue(self, msg=None, env={}):
-        if not self._socket:
+        if self._socket._closed:
             raise SessionError("Session has been closed.")
         seq = self._next_sequence()
         env.update({"seq": 0}) # fixed here
@@ -62,12 +77,12 @@ class Session:
             sys.stdout.write("[Session] enqueue: " + str(que.dump()) + "\n")
         return seq
 
-    def dequeue(self, seq=0):
-        if not self._socket:
+    def dequeue(self):
+        if self._socket._closed:
             raise SessionError("Session has been closed.")
         que = None
         try:
-            que = self.message_queue.pop(seq)
+            que = self.message_queue.pop(0) # always pop at index 0
             self.old_message_queue.append(que)
         except IndexError:
             que = Queue()
@@ -76,7 +91,7 @@ class Session:
         return que
 
     def get_queue(self, seq=None):
-        if not self._socket:
+        if self._socket._closed:
             raise SessionError("Session has been closed.")
         if seq is None:
             seq = len(self.message_queue) - 1
@@ -99,7 +114,7 @@ class Session:
                 "instance": instance })
 
     def group_recvmsg(self, nonblock=True, seq=0):
-        que = self.dequeue(seq)
+        que = self.dequeue()
         return que.msg, que.env
         
     def group_reply(self, routing, msg):
@@ -112,7 +127,7 @@ class Session:
                 "reply": routing["seq"] })
 
     def get_message(self, group, to='*'):
-        if not self._socket:
+        if self._socket._closed:
             raise SessionError("Session has been closed.")
         que = Queue()
         for q in self.message_queue:
@@ -124,3 +139,10 @@ class Session:
             sys.stdout.write("[Session] get_message: " + str(que.dump()) + "\n")
         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.")

+ 53 - 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
 # purpose with or without fee is hereby granted, provided that the above
@@ -13,16 +13,22 @@
 # 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 class of isc.cc.session
+"""
+A mock-up module of isc.cc.session
 
-__version__ = "$Revision$"
+*** NOTE ***
+It is only for testing stats_httpd module and not reusable for
+external module.
+"""
 
 import json
+import os
 from isc.cc.session import Session
 
 COMMAND_CONFIG_UPDATE = "config_update"
 
 def parse_answer(msg):
+    assert 'result' in msg
     try:
         return msg['result'][0], msg['result'][1]
     except IndexError:
@@ -35,6 +41,7 @@ def create_answer(rcode, arg = None):
         return { 'result': [ rcode, arg ] }
 
 def parse_command(msg):
+    assert 'command' in msg
     try:
         return msg['command'][0], msg['command'][1]
     except IndexError:
@@ -47,9 +54,21 @@ def create_command(command_name, params = None):
         return {"command": [command_name, params]}
 
 def module_spec_from_file(spec_file, check = True):
-    file = open(spec_file)
-    module_spec = json.loads(file.read())
-    return ModuleSpec(module_spec['module_spec'], check)
+    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:
     def __init__(self, module_spec, check = True):
@@ -67,10 +86,34 @@ class ModuleSpec:
 class ModuleCCSessionError(Exception):
     pass
 
+class DataNotFoundError(Exception):
+    pass
+
 class ConfigData:
     def __init__(self, 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 absolutely False
+           even if the value is an unset default or not. Raises an
+           DataNotFoundError if the identifier is not found in the
+           specification file.
+           *** NOTE ***
+           There are some differences from the original method. This
+           method never handles local settings like the original
+           method. But these different behaviors aren't so big issues
+           for a mock-up method of stats_httpd because stats_httpd
+           calls this method at only first."""
+        for config_map in self.get_module_spec().get_config_spec():
+            if config_map['item_name'] == identifier:
+                if 'item_default' in config_map:
+                    return config_map['item_default'], False
+        raise DataNotFoundError("item_name %s is not found in the specfile" % identifier)
+
+    def get_module_spec(self):
+        return self.specification
+
 class ModuleCCSession(ConfigData):
     def __init__(self, spec_file_name, config_handler, command_handler, cc_session = None):
         module_spec = module_spec_from_file(spec_file_name)
@@ -111,3 +154,7 @@ class ModuleCCSession(ConfigData):
 
     def get_module_spec(self):
         return self.specification
+
+    def get_socket(self):
+        return self._session._socket
+

+ 4 - 1
src/bin/stats/tests/isc/util/process.py

@@ -13,6 +13,9 @@
 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
-# A dummy function of isc.util.process.rename()
+"""
+A dummy function of isc.util.process.rename()
+"""
+
 def rename(name=None):
     pass

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

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

+ 0 - 18
src/bin/stats/tests/isc/utils/process.py

@@ -1,18 +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.
-
-# A dummy function of isc.utils.process.rename()
-def rename(name=None):
-    pass

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

@@ -1,6 +1,6 @@
 #! /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
 # 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
 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
 
 cd ${TEST_PATH}
 ${PYTHON_EXEC} -O b10-stats_test.py $*
-${PYTHON_EXEC} -O b10-stats_stub_test.py $*
+${PYTHON_EXEC} -O b10-stats-httpd_test.py $*