Browse Source

sync with trunk for merge

git-svn-id: svn://bind10.isc.org/svn/bind10/branches/trac172@2362 e5f2f494-b856-4b98-b285-d166d9295462
Jelte Jansen 15 years ago
parent
commit
1b93102619
100 changed files with 2158 additions and 700 deletions
  1. 94 3
      ChangeLog
  2. 57 58
      configure.ac
  3. 6 5
      src/bin/auth/Makefile.am
  4. 16 13
      src/bin/auth/asio_link.cc
  5. 1 1
      src/bin/auth/asio_link.h
  6. 16 2
      src/bin/auth/auth_srv.cc
  7. 2 2
      src/bin/auth/main.cc
  8. 2 2
      src/bin/auth/tests/Makefile.am
  9. 110 30
      src/bin/bind10/bind10.py.in
  10. 2 1
      src/bin/bind10/run_bind10.sh.in
  11. 134 0
      src/bin/bind10/tests/args_test.py
  12. 2 1
      src/bin/bind10/tests/bind10_test.in
  13. 0 13
      src/bin/bindctl/Makefile.am
  14. 1 0
      src/bin/bindctl/TODO
  15. 131 80
      src/bin/bindctl/bindcmd.py
  16. 8 12
      src/bin/bindctl/bindctl-source.py.in
  17. 0 36
      src/bin/bindctl/bindctl.pem
  18. 8 5
      src/bin/bindctl/cmdparse.py
  19. 7 0
      src/bin/bindctl/exception.py
  20. 48 4
      src/bin/bindctl/tests/bindctl_test.py
  21. 3 1
      src/bin/cfgmgr/Makefile.am
  22. 7 1
      src/bin/cfgmgr/b10-cfgmgr.py.in
  23. 13 0
      src/bin/cfgmgr/tests/Makefile.am
  24. 95 0
      src/bin/cfgmgr/tests/b10-cfgmgr_test.py.in
  25. 4 2
      src/bin/cmdctl/Makefile.am
  26. 3 5
      src/bin/cmdctl/TODO
  27. 269 137
      src/bin/cmdctl/cmdctl.py.in
  28. 5 15
      src/bin/cmdctl/cmdctl.spec
  29. 2 0
      src/bin/cmdctl/tests/Makefile.am
  30. 2 2
      src/bin/cmdctl/tests/cmdctl_test.in
  31. 185 20
      src/bin/cmdctl/tests/cmdctl_test.py
  32. 1 1
      src/bin/host/Makefile.am
  33. 2 0
      src/bin/host/host.cc
  34. 26 20
      src/bin/loadzone/Makefile.am
  35. 16 6
      src/bin/loadzone/b10-loadzone.py.in
  36. 1 1
      src/bin/loadzone/run_loadzone.sh.in
  37. 25 0
      src/bin/loadzone/tests/correct/Makefile.am
  38. 75 0
      src/bin/loadzone/tests/correct/correct_test.sh.in
  39. 14 0
      src/bin/loadzone/tests/correct/example.db
  40. 8 0
      src/bin/loadzone/tests/correct/get_zonedatas.py
  41. 4 0
      src/bin/loadzone/tests/correct/inclsub.db
  42. 23 0
      src/bin/loadzone/tests/correct/include.db
  43. 79 0
      src/bin/loadzone/tests/correct/known.test.out
  44. 21 0
      src/bin/loadzone/tests/correct/mix1.db
  45. 3 0
      src/bin/loadzone/tests/correct/mix1sub1.db
  46. 3 0
      src/bin/loadzone/tests/correct/mix1sub2.db
  47. 21 0
      src/bin/loadzone/tests/correct/mix2.db
  48. 3 0
      src/bin/loadzone/tests/correct/mix2sub1.txt
  49. 3 0
      src/bin/loadzone/tests/correct/mix2sub2.txt
  50. 17 0
      src/bin/loadzone/tests/correct/ttl1.db
  51. 17 0
      src/bin/loadzone/tests/correct/ttl2.db
  52. 18 0
      src/bin/loadzone/tests/correct/ttlext.db
  53. 25 0
      src/bin/loadzone/tests/error/Makefile.am
  54. 11 0
      src/bin/loadzone/tests/error/error.known
  55. 82 0
      src/bin/loadzone/tests/error/error_test.sh.in
  56. 13 0
      src/bin/loadzone/tests/error/formerr1.db
  57. 12 0
      src/bin/loadzone/tests/error/formerr2.db
  58. 12 0
      src/bin/loadzone/tests/error/formerr3.db
  59. 12 0
      src/bin/loadzone/tests/error/formerr4.db
  60. 13 0
      src/bin/loadzone/tests/error/formerr5.db
  61. 1 0
      src/bin/loadzone/tests/error/include.txt
  62. 12 0
      src/bin/loadzone/tests/error/keyerror1.db
  63. 12 0
      src/bin/loadzone/tests/error/keyerror2.db
  64. 13 0
      src/bin/loadzone/tests/error/keyerror3.db
  65. 11 0
      src/bin/loadzone/tests/error/originerr1.db
  66. 12 0
      src/bin/loadzone/tests/error/originerr2.db
  67. 0 0
      src/bin/loadzone/tests/normal/Kexample.com.+005+04456.key
  68. 0 0
      src/bin/loadzone/tests/normal/Kexample.com.+005+04456.private
  69. 0 0
      src/bin/loadzone/tests/normal/Kexample.com.+005+33495.key
  70. 0 0
      src/bin/loadzone/tests/normal/Kexample.com.+005+33495.private
  71. 0 0
      src/bin/loadzone/tests/normal/Ksql1.example.com.+005+12447.key
  72. 0 0
      src/bin/loadzone/tests/normal/Ksql1.example.com.+005+12447.private
  73. 0 0
      src/bin/loadzone/tests/normal/Ksql1.example.com.+005+33313.key
  74. 0 0
      src/bin/loadzone/tests/normal/Ksql1.example.com.+005+33313.private
  75. 0 0
      src/bin/loadzone/tests/normal/Ksql2.example.com.+005+38482.key
  76. 0 0
      src/bin/loadzone/tests/normal/Ksql2.example.com.+005+38482.private
  77. 0 0
      src/bin/loadzone/tests/normal/Ksql2.example.com.+005+63192.key
  78. 0 0
      src/bin/loadzone/tests/normal/Ksql2.example.com.+005+63192.private
  79. 0 0
      src/bin/loadzone/tests/normal/README
  80. 0 0
      src/bin/loadzone/tests/normal/dsset-subzone.example.com
  81. 0 0
      src/bin/loadzone/tests/normal/example.com
  82. 0 0
      src/bin/loadzone/tests/normal/example.com.signed
  83. 0 0
      src/bin/loadzone/tests/normal/sql1.example.com
  84. 0 0
      src/bin/loadzone/tests/normal/sql1.example.com.signed
  85. 0 0
      src/bin/loadzone/tests/normal/sql2.example.com
  86. 0 0
      src/bin/loadzone/tests/normal/sql2.example.com.signed
  87. 1 5
      src/bin/xfrin/tests/Makefile.am
  88. 43 41
      src/bin/xfrin/tests/xfrin_test.py
  89. 46 39
      src/bin/xfrin/xfrin.py.in
  90. 1 1
      src/bin/xfrout/run_b10-xfrout.sh.in
  91. 1 5
      src/bin/xfrout/tests/Makefile.am
  92. 37 31
      src/bin/xfrout/tests/xfrout_test.py
  93. 117 85
      src/bin/xfrout/xfrout.py.in
  94. 31 1
      src/bin/xfrout/xfrout.spec.pre.in
  95. 1 4
      src/lib/Makefile.am
  96. 6 4
      src/lib/cc/Makefile.am
  97. 11 4
      src/lib/cc/session.cc
  98. 4 1
      src/lib/cc/session_unittests.cc
  99. 5 0
      src/lib/config/tests/Makefile.am
  100. 0 0
      src/lib/datasrc/data_source.cc

+ 94 - 3
ChangeLog

@@ -1,11 +1,98 @@
-  53.   [bug]      zhanglikun
+  68.  [func]	    zhanglikun
+	Add options -c(--certificate-chain) to bindctl. Override class
+	HTTPSConnection to support server certificate validation.
+	Add support to cmdctl.spec file, now there are three configurable 
+	items for cmdctl: 'key_file', 'cert_file' and 'accounts_file', 
+	all of them can be changed in runtime.
+	(Trac #127, svn r2357)
+
+  67.  [func]	    zhanglikun
+	Make bindctl's command parser only do minimal check. Parameter 
+	value can be a sequence of non-space characters, or a string 
+	surrounded by quotation	marks(these marks can be a part of the 
+	value string in escaped form). Make	error message be more 
+	friendly.(if there is some error in parameter's value, the 
+	parameter name will be provided). Refactor function login_to_cmdctl() 
+	in class BindCmdInterpreter: avoid using Exception to catch all 
+	exceptions.
+	(Trac #220, svn r2356)
+
+  66.  [bug]        each
+	Check for duplicate RRsets before inserting data into a message
+	section; this, among other things, will prevent multiple copies
+	of the same CNAME from showing up when there's a loop.  (Trac #69,
+	svn r2350)
+    
+  65.  [func]       shentingting
+	Added verbose options to exactly what is happening with loadzone.
+	Added loadzone test suite of different file formats to load.
+	(Trac #197, #199, #244, #161, #198, #174, #175, svn r2340)
+
+  64.  [func]       jerry
+	Added python logging framework. It is for testing and experimenting
+	with logging ideas. Currently, it supports three channels(file, 
+	syslog and stderr) and five levels(debug, info, warning, error and
+	critical).
+	(Trac #176, svn r2338)
+
+  63.  [func]       shane
+	Added initial support for setuid(), using the "-u" flag. This will
+	be replaced in the future, but for now provides a reasonable 
+	starting point.
+	(Trac #180, svn r2330)
+
+  62.  [func]		jelte
+	bin/xfrin: Use the database_file as configured in Auth to transfers
+	bin/xfrout: Use the database_file as configured in Auth to transfers
+
+  61.  [bug]		jelte
+	bin/auth: Enable b10-auth to be launched in source tree
+	(i.e. use a zone database file relative to that)
+
+  60.	[build]		jinmei
+	Supported SunStudio C++ compiler.  Note: gtest still doesn't work.
+	(Trac #251, svn r2310)
+
+  59.	[bug]		jinmei
+	lib/datasrc,bin/auth: The authoritative server could return a
+	SERVFAIL with a partial answer if it finds a data source broken
+	while looking for an answer.  This can happen, for example, if a
+	zone that doesn't have an NS RR is configured and loaded as a
+	sqlite3 data source. (Trac #249, r2286)
+
+  58.	[bug]		jinmei
+	Worked around an interaction issue between ASIO and standard C++
+	library headers.  Without this ASIO didn't work: sometimes the
+	application crashes, sometimes it blocked in the ASIO module.
+	(Trac #248, svn r2187, r2190)
+
+  57.	[func]		jinmei
+	lib/datasrc: used a simpler version of Name::split (change 31) for
+	better readability.  No behavior change. (Trac #200, svn r2159)
+
+  56.	[func]*		jinmei
+	lib/dns: renamed the library name to libdns++ to avoid confusion
+	with the same name of library of BIND 9.
+	(Trac #190, svn r2153)
+
+  55.	[bug]		shane
+	bin/xfrout: xfrout exception on Ctrl-C now no longer generates
+	exception for 'Interrupted system call'
+	(Track #136, svn r2147)
+
+  54.	[bug]		zhanglikun
+	bin/xfrout: Enable b10-xfrout can be launched in source
+	code tree.
+	(Trac #224, svn r2103)
+
+  53.	[bug]		zhanglikun
 	bin/bindctl: Generate a unique session ID by using 
 	socket.gethostname() instead of socket.gethostbyname(), 
 	since the latter one could make bindctl	stall if its own 
 	host name can't be resolved.
 	(Trac #228, svn r2096)
 
-  52.   [func]      zhanglikun
+  52.	[func]		zhanglikun
 	bin/xfrout: When xfrout is launched, check whether the
 	socket file is being used by one running xfrout process, 
 	if it is, exit from python.	If the file isn't a socket file 
@@ -67,6 +154,10 @@ bind10-devel-20100602 released on June 2, 2010
 	Renamed libauth to libdatasrc.
 
   38.   [bug]           zhanglikun
+	Send command 'shutdown' to Xfrin and Xfrout when boss receive SIGINT.
+	Remove unused socket file when Xfrout process exits. Make sure Xfrout
+	exit by itself when it receives SIGINT, instead of being killed by the
+	signal SIGTERM or SIGKILL sent from boss.
 	(Trac #135, #151, #134, svn r1797)
 
   37.   [build]         jinmei
@@ -127,7 +218,7 @@ bind10-devel-20100421 released on April 21, 2010
 
   24.	[func]
 	Support case-sensitive name compression in MessageRenderer.
-	(svn r1704)
+	(Trac #142, svn r1704)
 
   23.	[func]
 	Support a simple name with possible compression. (svn r1701)

+ 57 - 58
configure.ac

@@ -9,11 +9,23 @@ AC_CONFIG_HEADERS([config.h])
 
 # Checks for programs.
 AC_PROG_CXX
-AC_PROG_CC
 AC_PROG_LIBTOOL
 
 # Use C++ language
-AC_LANG_CPLUSPLUS
+AC_LANG([C++])
+
+# Identify the compiler: this check must be after AC_PROG_CXX and AC_LANG.
+AM_CONDITIONAL(USE_GXX, test "X${GXX}" = "Xyes")
+AC_CHECK_DECL([__SUNPRO_CC], [SUNCXX="yes"], [SUNCXX="no"])
+
+# OS dependent compiler flags
+case "$host" in
+*-solaris*)
+	# Solaris requires special definitions to get some standard libraries
+	# (e.g. getopt(3)) available with common used header files.
+	CPPFLAGS="$CPPFLAGS -D_XPG4_2 -D__EXTENSIONS__"
+	;;
+esac
 
 m4_define([_AM_PYTHON_INTERPRETER_LIST], [python python3 python3.1])
 AC_ARG_WITH([pythonpath],
@@ -74,6 +86,12 @@ fi
 AC_SUBST(PYTHON_INCLUDES)
 AC_SUBST(PYTHON_LDFLAGS)
 
+CPPFLAGS_SAVED="$CPPFLAGS"
+CPPFLAGS="$CPPFLAGS ${PYTHON_INCLUDES}"
+AC_CHECK_HEADERS([Python.h],, AC_MSG_ERROR([Missing Python.h]))
+CPPFLAGS="$CPPFLAGS_SAVED"
+
+
 # Check for python library (not absolutely mandatory, but needed for
 # Boost.Python when we use it.  See below.)
 LDFLAGS_SAVED="$LDFLAGS"
@@ -87,8 +105,8 @@ AC_SUBST(PYTHON_LIB)
 
 # TODO: check for _sqlite3.py module
 
-#
-# B10_CXXFLAGS is the default C++ compiler flags.  This will (and should) be
+# Compiler dependent settings: define some mandatory CXXFLAGS here.
+# We also use a separate variable B10_CXXFLAGS.  This will (and should) be
 # used as the default value for each specifc AM_CXXFLAGS:
 # AM_CXXFLAGS = $(B10_CXXFLAGS)
 # AM_CXXFLAGS += ... # add module specific flags
@@ -97,17 +115,24 @@ AC_SUBST(PYTHON_LIB)
 # gcc's -Wno-XXX option must be specified after -Wall or -Wextra, we cannot
 # specify the default warning flags in CXXFLAGS and let specific modules
 # "override" the default.
-#
-B10_CXXFLAGS=
 
-if test "X$GCC" = "Xyes"; then
-B10_CXXFLAGS="-g -Wall -Wextra -Wwrite-strings -Woverloaded-virtual -Wno-sign-compare"
+CXXFLAGS=-g
+werror_ok=0
+
+# SunStudio compiler requires special compiler options for boost
+# (http://blogs.sun.com/sga/entry/boost_mini_howto)
+if test "$SUNCXX" = "yes"; then
+CXXFLAGS="$CXXFLAGS -library=stlport4 -features=tmplife -features=tmplrefstatic"
+fi
+
+# gcc specific settings:
+if test "X$GXX" = "Xyes"; then
+B10_CXXFLAGS="-Wall -Wextra -Wwrite-strings -Woverloaded-virtual -Wno-sign-compare"
 UNUSED_PARAM_ATTRIBUTE='__attribute__((unused))'
 
 # Certain versions of gcc (g++) have a bug that incorrectly warns about
 # the use of anonymous name spaces even if they're closed in a single
 # translation unit.  For these versions we have to disable -Werror.
-werror_ok=0
 CXXFLAGS_SAVED="$CXXFLAGS"
 CXXFLAGS="$CXXFLAGS $B10_CXXFLAGS -Werror"
 AC_MSG_CHECKING(for in-TU anonymous namespace breakage)
@@ -118,13 +143,13 @@ namespace isc {class Bar {Foo foo_;};} ],,
 	 B10_CXXFLAGS="$B10_CXXFLAGS -Werror"],
 	[AC_MSG_RESULT(yes)])
 CXXFLAGS="$CXXFLAGS_SAVED"
-fi				dnl GCC = yes
+fi				dnl GXX = yes
 
 AM_CONDITIONAL(GCC_WERROR_OK, test $werror_ok = 1)
 AC_DEFINE_UNQUOTED(UNUSED_PARAM, $UNUSED_PARAM_ATTRIBUTE, Define to compiler keyword indicating a function argument is intentionally unused)
 
 # produce PIC unless we disable shared libraries. need this for python bindings.
-if test $enable_shared != "no" -a "X$GCC" = "Xyes"; then
+if test $enable_shared != "no" -a "X$GXX" = "Xyes"; then
    B10_CXXFLAGS="$B10_CXXFLAGS -fPIC"
 fi
 
@@ -213,52 +238,6 @@ AC_SUBST(BOOST_LDFLAGS)
 
 # Check availability of the Boost Python library
 
-AC_MSG_CHECKING([for boost::python library])
-AC_ARG_WITH([boost-python],
-AC_HELP_STRING([--with-boost-python],
-  [specify whether to use the boost python library]),
-  [with_boost_python="$withval"], [with_boost_python="auto"])
-if test "$with_boost_python" != "no"; then
-	if test "$with_boost_python" != "auto" -a "X$PYTHON_LIB" = X; then
-		AC_MSG_ERROR([Boost.Python requested but python library is not available])
-	fi
-	LDFLAGS_SAVED="$LDFLAGS"
-	LIBS_SAVED="$LIBS"
-	CPPFLAGS_SAVED="$CPPFLAGS"
-	CPPFLAGS="$CPPFLAGS $PYTHON_INCLUDES"
-
-	for BOOST_TRY_LIB in boost_python boost_python-mt; do
-		LDFLAGS="$LDFLAGS_SAVED ${BOOST_LDFLAGS} ${PYTHON_LDFLAGS}"
-		LIBS="$LIBS_SAVED -l${BOOST_TRY_LIB} ${PYTHON_LIB}"
-		AC_TRY_LINK([#include <boost/python/module.hpp>
-	      using namespace boost::python;
-	      BOOST_PYTHON_MODULE(test) { throw "Boost::Python test."; }],
-			[ return 0; ],
-			[ AC_MSG_RESULT(yes)
-			BOOST_PYTHON_LIB="-l${BOOST_TRY_LIB}"
-			],[])
-		if test "X${BOOST_PYTHON_LIB}" != X; then
-        		break
-		fi
-	done
-
-	LDFLAGS="$LDFLAGS_SAVED"
-	CPPFLAGS="$CPPFLAGS_SAVED"
-	LIBS="$LIBS_SAVED"
-fi
-
-if test "X${BOOST_PYTHON_LIB}" = X; then
-	AC_MSG_RESULT(no)
-	if test "$with_boost_python" = "yes"; then
-	   AC_MSG_ERROR([boost python library is requested but not found])
-	fi
-else
-	AC_DEFINE(HAVE_BOOST_PYTHON, 1, Define to 1 if boost python library is available)
-fi
-
-AM_CONDITIONAL(HAVE_BOOST_PYTHON, test "X${BOOST_PYTHON_LIB}" != X)
-AC_SUBST(BOOST_PYTHON_LIB)
-
 #
 # Check availability of gtest, which will be used for unit tests.
 #
@@ -310,6 +289,11 @@ AC_SUBST(GTEST_INCLUDES)
 AC_SUBST(GTEST_LDFLAGS)
 AC_SUBST(GTEST_LDADD)
 
+dnl check for pkg-config itself so we don't try the m4 macro without pkg-config
+AC_CHECK_PROG(HAVE_PKG_CONFIG, pkg-config, yes, no)
+if test "x$HAVE_PKG_CONFIG" = "xno" ; then
+  AC_MSG_ERROR(Please install pkg-config)
+fi
 PKG_CHECK_MODULES(SQLITE, sqlite3 >= 3.3.9, enable_features="$enable_features SQLite3")
 
 # I can't get some of the #include <asio.hpp> right without this
@@ -359,7 +343,7 @@ fi
 # run time performance.  Hpefully we can find a better solution or the ASIO
 # code will be updated by the time we really need it.
 AC_CHECK_HEADERS(sys/devpoll.h, ac_cv_have_devpoll=yes, ac_cv_have_devpoll=no)
-if test "X$ac_cv_have_devpoll" = "Xyes" -a "X$GCC" = "Xyes"; then
+if test "X$ac_cv_have_devpoll" = "Xyes" -a "X$GXX" = "Xyes"; then
 	CPPFLAGS="$CPPFLAGS -DASIO_DISABLE_DEV_POLL=1"
 fi
 
@@ -388,8 +372,11 @@ AC_CONFIG_FILES([Makefile
                  src/bin/bindctl/Makefile
                  src/bin/bindctl/tests/Makefile
                  src/bin/cfgmgr/Makefile
+                 src/bin/cfgmgr/tests/Makefile
                  src/bin/host/Makefile
                  src/bin/loadzone/Makefile
+                 src/bin/loadzone/tests/correct/Makefile
+                 src/bin/loadzone/tests/error/Makefile
                  src/bin/msgq/Makefile
                  src/bin/msgq/tests/Makefile
                  src/bin/auth/Makefile
@@ -408,19 +395,25 @@ AC_CONFIG_FILES([Makefile
                  src/lib/python/isc/cc/tests/Makefile
                  src/lib/python/isc/config/Makefile
                  src/lib/python/isc/config/tests/Makefile
+                 src/lib/python/isc/log/Makefile
+                 src/lib/python/isc/log/tests/Makefile
                  src/lib/config/Makefile
                  src/lib/config/tests/Makefile
                  src/lib/dns/Makefile
                  src/lib/dns/tests/Makefile
+                 src/lib/dns/python/Makefile
+                 src/lib/dns/python/tests/Makefile
                  src/lib/exceptions/Makefile
                  src/lib/datasrc/Makefile
                  src/lib/datasrc/tests/Makefile
                  src/lib/xfr/Makefile
                ])
 AC_OUTPUT([src/bin/cfgmgr/b10-cfgmgr.py
+           src/bin/cfgmgr/tests/b10-cfgmgr_test.py
            src/bin/cmdctl/cmdctl.py
            src/bin/cmdctl/run_b10-cmdctl.sh
            src/bin/cmdctl/tests/cmdctl_test
+           src/bin/cmdctl/cmdctl.spec.pre
            src/bin/xfrin/tests/xfrin_test
            src/bin/xfrin/xfrin.py
            src/bin/xfrin/xfrin.spec.pre
@@ -436,6 +429,8 @@ AC_OUTPUT([src/bin/cfgmgr/b10-cfgmgr.py
            src/bin/bindctl/bindctl-source.py
            src/bin/bindctl/tests/bindctl_test
            src/bin/loadzone/run_loadzone.sh
+           src/bin/loadzone/tests/correct/correct_test.sh
+           src/bin/loadzone/tests/error/error_test.sh
            src/bin/loadzone/b10-loadzone.py
            src/bin/usermgr/run_b10-cmdctl-usermgr.sh
            src/bin/usermgr/b10-cmdctl-usermgr.py
@@ -447,6 +442,7 @@ AC_OUTPUT([src/bin/cfgmgr/b10-cfgmgr.py
            src/lib/config/tests/data_def_unittests_config.h
            src/lib/python/isc/config/tests/config_test
            src/lib/python/isc/cc/tests/cc_test
+           src/lib/python/isc/log/tests/log_test
            src/lib/dns/gen-rdatacode.py
            src/lib/python/bind10_config.py
            src/lib/dns/tests/testdata/gen-wiredata.py
@@ -462,11 +458,14 @@ AC_OUTPUT([src/bin/cfgmgr/b10-cfgmgr.py
            chmod +x src/bin/bindctl/tests/bindctl_test
            chmod +x src/bin/bindctl/run_bindctl.sh
            chmod +x src/bin/loadzone/run_loadzone.sh
+           chmod +x src/bin/loadzone/tests/correct/correct_test.sh
+           chmod +x src/bin/loadzone/tests/error/error_test.sh
            chmod +x src/bin/usermgr/run_b10-cmdctl-usermgr.sh
            chmod +x src/bin/msgq/run_msgq.sh
            chmod +x src/bin/msgq/tests/msgq_test
            chmod +x src/lib/dns/gen-rdatacode.py
            chmod +x src/lib/dns/tests/testdata/gen-wiredata.py
+           chmod +x src/lib/dns/python/tests/libdns_python_test
           ])
 AC_OUTPUT
 

+ 6 - 5
src/bin/auth/Makefile.am

@@ -36,7 +36,10 @@ lib_LIBRARIES = libasio_link.a
 libasio_link_a_SOURCES = asio_link.cc asio_link.h
 # Note: the ordering matters: -Wno-... must follow -Wextra (defined in
 # B10_CXXFLAGS)
-libasio_link_a_CXXFLAGS = $(AM_CXXFLAGS) -Wno-unused-parameter
+libasio_link_a_CXXFLAGS = $(AM_CXXFLAGS)
+if USE_GXX
+libasio_link_a_CXXFLAGS += -Wno-unused-parameter
+endif
 libasio_link_a_CPPFLAGS = $(AM_CPPFLAGS)
 
 BUILT_SOURCES = spec_config.h 
@@ -45,15 +48,13 @@ b10_auth_SOURCES = auth_srv.cc auth_srv.h
 b10_auth_SOURCES += common.h
 b10_auth_SOURCES += main.cc
 b10_auth_LDADD =  $(top_builddir)/src/lib/datasrc/.libs/libdatasrc.a
-b10_auth_LDADD += $(top_builddir)/src/lib/dns/.libs/libdns.a
+b10_auth_LDADD += $(top_builddir)/src/lib/dns/.libs/libdns++.a
 b10_auth_LDADD += $(top_builddir)/src/lib/config/.libs/libcfgclient.a
-b10_auth_LDADD += $(top_builddir)/src/lib/cc/libcc.a
+b10_auth_LDADD += $(top_builddir)/src/lib/cc/.libs/libcc.a
 b10_auth_LDADD += $(top_builddir)/src/lib/exceptions/.libs/libexceptions.a
 b10_auth_LDADD += $(top_builddir)/src/bin/auth/libasio_link.a
 b10_auth_LDADD += $(SQLITE_LIBS)
-if HAVE_BOOST_PYTHON
 b10_auth_LDADD += $(top_builddir)/src/lib/xfr/.libs/libxfr.a
-endif
 
 # TODO: config.h.in is wrong because doesn't honor pkgdatadir
 # and can't use @datadir@ because doesn't expand default ${prefix}

+ 16 - 13
src/bin/auth/asio_link.cc

@@ -16,6 +16,7 @@
 
 #include <config.h>
 
+#include <unistd.h>             // for some IPC/network system calls
 #include <asio.hpp>
 #include <boost/bind.hpp>
 
@@ -23,10 +24,7 @@
 #include <dns/message.h>
 #include <dns/messagerenderer.h>
 
-#if defined(HAVE_BOOST_PYTHON)
-#define USE_XFROUT
 #include <xfr/xfrout_client.h>
-#endif
 
 #include <asio_link.h>
 
@@ -39,15 +37,15 @@ using ip::tcp;
 
 using namespace std;
 using namespace isc::dns;
-#ifdef USE_XFROUT
 using namespace isc::xfr;
-#endif
 
 namespace {
 // As a short term workaround, we have XFROUT specific code.  We should soon
 // refactor the code with some abstraction so that we can separate this level
 // details from the (AS)IO module.
-#ifdef USE_XFROUT
+
+// This was contained in an ifdef USE_XFROUT, but we should really check
+// live if we do xfrout
 //TODO. The sample way for checking axfr query, the code should be merged to auth server class
 bool
 check_axfr_query(char* const msg_data, const uint16_t msg_len) {
@@ -64,11 +62,21 @@ check_axfr_query(char* const msg_data, const uint16_t msg_len) {
 }
 
 //TODO. Send the xfr query to xfrout module, the code should be merged to auth server class
+//BIGGERTODO: stop using hardcoded install-path locations! 
 void
 dispatch_axfr_query(const int tcp_sock, char const axfr_query[],
                     const uint16_t query_len)
 {
-    string path(UNIX_SOCKET_FILE);
+    string path;
+    if (getenv("B10_FROM_BUILD")) {
+        path = string(getenv("B10_FROM_BUILD")) + "/auth_xfrout_conn";
+    } else {
+        path = UNIX_SOCKET_FILE;
+    }
+    
+    if (getenv("B10_FROM_BUILD")) {
+        path = string(getenv("B10_FROM_BUILD")) + "/auth_xfrout_conn";
+    }
     XfroutClient xfr_client(path);
     try {
         xfr_client.connect();
@@ -78,10 +86,9 @@ dispatch_axfr_query(const int tcp_sock, char const axfr_query[],
     }
     catch (const exception & err) {
         //if (verbose_mode)
-        cerr << "error handle xfr query:" << err.what() << endl;
+        cerr << "error handle xfr query " << UNIX_SOCKET_FILE << ":" << err.what() << endl;
     }
 }
-#endif
 }
 
 namespace asio_link {
@@ -134,13 +141,11 @@ public:
     {
         if (!error) {
             InputBuffer dnsbuffer(data_, bytes_transferred);
-#ifdef USE_XFROUT
             if (check_axfr_query(data_, bytes_transferred)) {
                 dispatch_axfr_query(socket_.native(), data_, bytes_transferred); 
                 // start to get new query ?
                 start();
             } else {
-#endif          
                 if (auth_server_->processMessage(dnsbuffer, dns_message_,
                                                 response_renderer_, false)) {
                     responselen_buffer_.writeUint16(
@@ -154,9 +159,7 @@ public:
                 } else {
                     delete this;
                 }
-#ifdef USE_XFROUT
             }
-#endif
         } else {
             delete this;
         }

+ 1 - 1
src/bin/auth/asio_link.h

@@ -24,7 +24,7 @@ struct IOServiceImpl;
 
 class IOService {
 public:
-    IOService(AuthSrv* auth_server, const char* port,
+    IOService(AuthSrv* auth_server, const char* const port,
               const bool use_ipv4, const bool use_ipv6);
     ~IOService();
     void run();

+ 16 - 2
src/bin/auth/auth_srv.cc

@@ -243,7 +243,8 @@ AuthSrv::processMessage(InputBuffer& request_buffer, Message& message,
         impl_->data_sources_.doQuery(query);
     } catch (const Exception& ex) {
         if (impl_->verbose_mode_) {
-            cerr << "[b10-auth] Internal error, returning SERVFAIL: " << ex.what() << endl;
+            cerr << "[b10-auth] Internal error, returning SERVFAIL: " <<
+                ex.what() << endl;
         }
         makeErrorMessage(message, response_renderer, Rcode::SERVFAIL(),
                          impl_->verbose_mode_);
@@ -273,9 +274,22 @@ AuthSrvImpl::setDbFile(const isc::data::ElementPtr config) {
         bool is_default;
         string item("database_file");
         ElementPtr value = cs_->getValue(is_default, item);
-        db_file_ = value->stringValue();
         final = Element::createMap();
+
+        // If the value is the default, and we are running from
+        // a specific directory ('from build'), we need to use
+        // a different value than the default (which may not exist)
+        // (btw, this should not be done here in the end, i think
+        //  the from-source script should have a check for this,
+        //  but for that we need offline access to config, so for
+        //  now this is a decent solution)
+        if (is_default && getenv("B10_FROM_BUILD")) {
+            value = Element::create(string(getenv("B10_FROM_BUILD")) +
+                                    "/bind10_zones.sqlite3");
+        }
         final->set(item, value);
+
+        db_file_ = value->stringValue();
     } else {
         return (answer);
     }

+ 2 - 2
src/bin/auth/main.cc

@@ -148,12 +148,12 @@ main(int argc, char* argv[]) {
         io_service = new asio_link::IOService(auth_server, port, use_ipv4,
                                               use_ipv6);
 
-        ModuleCCSession cs(specfile, io_service->get_io_service(), my_config_handler, my_command_handler);
+        ModuleCCSession cs(specfile, io_service->get_io_service(),
+                           my_config_handler, my_command_handler);
 
         auth_server->setConfigSession(&cs);
         auth_server->updateConfig(ElementPtr());
 
-        
         cout << "[b10-auth] Server started." << endl;
         io_service->run();
     } catch (const std::exception& ex) {

+ 2 - 2
src/bin/auth/tests/Makefile.am

@@ -20,9 +20,9 @@ run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
 run_unittests_LDADD = $(GTEST_LDADD)
 run_unittests_LDADD += $(SQLITE_LIBS)
 run_unittests_LDADD +=  $(top_builddir)/src/lib/datasrc/.libs/libdatasrc.a
-run_unittests_LDADD +=  $(top_builddir)/src/lib/dns/.libs/libdns.a
+run_unittests_LDADD +=  $(top_builddir)/src/lib/dns/.libs/libdns++.a
 run_unittests_LDADD += $(top_builddir)/src/lib/config/.libs/libcfgclient.a
-run_unittests_LDADD += $(top_builddir)/src/lib/cc/libcc.a
+run_unittests_LDADD += $(top_builddir)/src/lib/cc/.libs/libcc.a
 run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/.libs/libexceptions.a
 endif
 

+ 110 - 30
src/bin/bind10/bind10.py.in

@@ -57,6 +57,9 @@ import time
 import select
 import random
 from optparse import OptionParser, OptionValueError
+import io
+import pwd
+import posix
 
 import isc.cc
 
@@ -108,21 +111,38 @@ to avoid being restarted at exactly 10 seconds."""
             when = time.time()
         return max(when, self.restart_time)
 
+class ProcessInfoError(Exception): pass
+
 class ProcessInfo:
     """Information about a process"""
 
     dev_null = open(os.devnull, "w")
 
     def __init__(self, name, args, env={}, dev_null_stdout=False,
-                 dev_null_stderr=False):
+                 dev_null_stderr=False, uid=None, username=None):
         self.name = name 
         self.args = args
         self.env = env
         self.dev_null_stdout = dev_null_stdout
         self.dev_null_stderr = dev_null_stderr
         self.restart_schedule = RestartSchedule()
+        self.uid = uid
+        self.username = username
         self._spawn()
 
+    def _setuid(self):
+        """Function used before running a program that needs to run as a
+        different user."""
+        if self.uid is not None:
+            try:
+                posix.setuid(self.uid)
+            except OSError as e:
+                if e.errno == errno.EPERM:
+                    # if we failed to change user due to permission report that
+                    raise ProcessInfoError("Unable to change to user %s (uid %d)" % (self.username, self.uid))
+                else:
+                    # otherwise simply re-raise whatever error we found
+                    raise
 
     def _spawn(self):
         if self.dev_null_stdout:
@@ -138,14 +158,15 @@ class ProcessInfo:
         # on construction (self.env).
         spawn_env = os.environ
         spawn_env.update(self.env)
-        if not 'B10_FROM_SOURCE' in os.environ:
+        if 'B10_FROM_SOURCE' not in os.environ:
             spawn_env['PATH'] = "@@LIBEXECDIR@@:" + spawn_env['PATH']
         self.process = subprocess.Popen(self.args,
                                         stdin=subprocess.PIPE,
                                         stdout=spawn_stdout,
                                         stderr=spawn_stderr,
                                         close_fds=True,
-                                        env=spawn_env,)
+                                        env=spawn_env,
+                                        preexec_fn=self._setuid)
         self.pid = self.process.pid
         self.restart_schedule.set_run_start_time()
 
@@ -155,7 +176,8 @@ class ProcessInfo:
 class BoB:
     """Boss of BIND class."""
     
-    def __init__(self, msgq_socket_file=None, auth_port=5300, verbose=False):
+    def __init__(self, msgq_socket_file=None, auth_port=5300, verbose=False,
+                 setuid=None, username=None):
         """Initialize the Boss of BIND. This is a singleton (only one
         can run).
         
@@ -171,6 +193,8 @@ class BoB:
         self.processes = {}
         self.dead_processes = {}
         self.runnable = False
+        self.uid = setuid
+        self.username = username
 
     def config_handler(self, new_config):
         if self.verbose:
@@ -225,12 +249,14 @@ class BoB:
                 sys.stdout.write("[bind10] Starting b10-msgq\n")
         try:
             c_channel = ProcessInfo("b10-msgq", ["b10-msgq"], c_channel_env,
-                                    True, not self.verbose)
+                                    True, not self.verbose, uid=self.uid,
+                                    username=self.username)
         except Exception as e:
             return "Unable to start b10-msgq; " + str(e)
         self.processes[c_channel.pid] = c_channel
         if self.verbose:
-            sys.stdout.write("[bind10] Started b10-msgq (PID %d)\n" % c_channel.pid)
+            sys.stdout.write("[bind10] Started b10-msgq (PID %d)\n" % 
+                             c_channel.pid)
 
         # now connect to the c-channel
         cc_connect_start = time.time()
@@ -250,7 +276,8 @@ class BoB:
             sys.stdout.write("[bind10] Starting b10-cfgmgr\n")
         try:
             bind_cfgd = ProcessInfo("b10-cfgmgr", ["b10-cfgmgr"],
-                                    c_channel_env)
+                                    c_channel_env, uid=self.uid,
+                                    username=self.username)
         except Exception as e:
             c_channel.process.kill()
             return "Unable to start b10-cfgmgr; " + str(e)
@@ -272,23 +299,6 @@ class BoB:
         if self.verbose:
             sys.stdout.write("[bind10] ccsession started\n")
 
-        # start the xfrout before auth-server, to make sure every xfr-query can
-        # be processed properly.
-        xfrout_args = ['b10-xfrout']
-        if self.verbose:
-            sys.stdout.write("[bind10] Starting b10-xfrout\n")
-            xfrout_args += ['-v']
-        try:
-            xfrout = ProcessInfo("b10-xfrout", xfrout_args, 
-                                 c_channel_env )
-        except Exception as e:
-            c_channel.process.kill()
-            bind_cfgd.process.kill()
-            return "Unable to start b10-xfrout; " + str(e)
-        self.processes[xfrout.pid] = xfrout
-        if self.verbose:
-            sys.stdout.write("[bind10] Started b10-xfrout (PID %d)\n" % xfrout.pid)
-
         # start b10-auth
         # XXX: this must be read from the configuration manager in the future
         authargs = ['b10-auth', '-p', str(self.auth_port)]
@@ -308,6 +318,28 @@ class BoB:
         if self.verbose:
             sys.stdout.write("[bind10] Started b10-auth (PID %d)\n" % auth.pid)
 
+        # everything after the authoritative server can run as non-root
+        if self.uid is not None:
+            posix.setuid(self.uid)
+
+        # start the xfrout before auth-server, to make sure every xfr-query can
+        # be processed properly.
+        xfrout_args = ['b10-xfrout']
+        if self.verbose:
+            sys.stdout.write("[bind10] Starting b10-xfrout\n")
+            xfrout_args += ['-v']
+        try:
+            xfrout = ProcessInfo("b10-xfrout", xfrout_args, 
+                                 c_channel_env )
+        except Exception as e:
+            c_channel.process.kill()
+            bind_cfgd.process.kill()
+            return "Unable to start b10-xfrout; " + str(e)
+        self.processes[xfrout.pid] = xfrout
+        if self.verbose:
+            sys.stdout.write("[bind10] Started b10-xfrout (PID %d)\n" % 
+                             xfrout.pid)
+
         # start b10-xfrin
         xfrin_args = ['b10-xfrin']
         if self.verbose:
@@ -324,7 +356,8 @@ class BoB:
             return "Unable to start b10-xfrin; " + str(e)
         self.processes[xfrind.pid] = xfrind
         if self.verbose:
-            sys.stdout.write("[bind10] Started b10-xfrin (PID %d)\n" % xfrind.pid)
+            sys.stdout.write("[bind10] Started b10-xfrin (PID %d)\n" % 
+                             xfrind.pid)
 
         # start the b10-cmdctl
         # XXX: we hardcode port 8080
@@ -344,7 +377,8 @@ class BoB:
             return "Unable to start b10-cmdctl; " + str(e)
         self.processes[cmd_ctrld.pid] = cmd_ctrld
         if self.verbose:
-            sys.stdout.write("[bind10] Started b10-cmdctl (PID %d)\n" % cmd_ctrld.pid)
+            sys.stdout.write("[bind10] Started b10-cmdctl (PID %d)\n" % 
+                             cmd_ctrld.pid)
 
         self.runnable = True
 
@@ -353,7 +387,7 @@ class BoB:
     def stop_all_processes(self):
         """Stop all processes."""
         cmd = { "command": ['shutdown']}
-        self.cc_session.group_sendmsg(cmd, 'Boss', 'Cmd-Ctrld')
+        self.cc_session.group_sendmsg(cmd, 'Boss', 'Cmdctl')
         self.cc_session.group_sendmsg(cmd, "Boss", "ConfigManager")
         self.cc_session.group_sendmsg(cmd, "Boss", "Auth")
         self.cc_session.group_sendmsg(cmd, "Boss", "Xfrout")
@@ -435,11 +469,16 @@ class BoB:
                 sys.stdout.write("[bind10] Unknown child pid %d exited.\n" % pid)
 
     def restart_processes(self):
-        """Restart any dead processes."""
+        """Restart any dead processes.
+        Returns the time when the next process is ready to be restarted. 
+          If the server is shutting down, returns 0.
+          If there are no processes, returns None.
+        The values returned can be safely passed into select() as the 
+        timeout value."""
         next_restart = None
         # if we're shutting down, then don't restart
         if not self.runnable:
-            return next_restart
+            return 0
         # otherwise look through each dead process and try to restart
         still_dead = {}
         now = time.time()
@@ -510,6 +549,10 @@ def check_port(option, opt_str, value, parser):
 def main():
     global options
     global boss_of_bind
+    # Enforce line buffering on stdout, even when not a TTY
+    sys.stdout = io.TextIOWrapper(sys.stdout.detach(), line_buffering=True)
+
+
     # Parse any command-line options.
     parser = OptionParser(version=__version__)
     parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
@@ -520,7 +563,42 @@ def main():
     parser.add_option("-m", "--msgq-socket-file", dest="msgq_socket_file",
                       type="string", default=None,
                       help="UNIX domain socket file the b10-msgq daemon will use")
+    parser.add_option("-u", "--user", dest="user",
+                      type="string", default=None,
+                      help="Change user after startup (must run as root)")
     (options, args) = parser.parse_args()
+    if args:
+        parser.print_help()
+        sys.exit(1)
+
+    # Check user ID.
+    setuid = None
+    username = None
+    if options.user:
+        # Try getting information about the user, assuming UID passed.
+        try:
+            pw_ent = pwd.getpwuid(int(options.user))
+            setuid = pw_ent.pw_uid
+            username = pw_ent.pw_name
+        except ValueError:
+            pass
+        except KeyError:
+            pass
+
+        # Next try getting information about the user, assuming user name 
+        # passed.
+        # If the information is both a valid user name and user number, we
+        # prefer the name because we try it second. A minor point, hopefully.
+        try:
+            pw_ent = pwd.getpwnam(options.user)
+            setuid = pw_ent.pw_uid
+            username = pw_ent.pw_name
+        except KeyError:
+            pass
+
+        if setuid is None:
+            sys.stderr.write("bind10: invalid user: '%s'\n" % options.user)
+            sys.exit(1)
 
     # Announce startup.
     if options.verbose:
@@ -543,11 +621,12 @@ def main():
 
     # Go bob!
     boss_of_bind = BoB(options.msgq_socket_file, int(options.auth_port),
-                       options.verbose)
+                       options.verbose, setuid, username)
     startup_result = boss_of_bind.startup()
     if startup_result:
         sys.stderr.write("[bind10] Error on startup: %s\n" % startup_result)
         sys.exit(1)
+    sys.stdout.write("[bind10] BIND 10 started\n")
 
     # In our main loop, we check for dead processes or messages 
     # on the c-channel.
@@ -584,6 +663,7 @@ def main():
     # shutdown
     signal.signal(signal.SIGCHLD, signal.SIG_DFL)
     boss_of_bind.shutdown()
+    sys.exit(0)
 
 if __name__ == "__main__":
     main()

+ 2 - 1
src/bin/bind10/run_bind10.sh.in

@@ -23,7 +23,8 @@ BIND10_PATH=@abs_top_builddir@/src/bin/bind10
 PATH=@abs_top_builddir@/src/bin/msgq:@abs_top_builddir@/src/bin/auth:@abs_top_builddir@/src/bin/cfgmgr:@abs_top_builddir@/src/bin/cmdctl:@abs_top_builddir@/src/bin/xfrin:@abs_top_builddir@/src/bin/xfrout:$PATH
 export PATH
 
-PYTHONPATH=@abs_top_srcdir@/src/lib/python:@abs_top_builddir@/src/lib/python:@abs_top_builddir@/src/lib/dns/.libs:@abs_top_builddir@/src/lib/xfr/.libs
+PYTHONPATH=@abs_top_builddir@/src/lib/python:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/xfr/.libs
+#PYTHONPATH=@abs_top_srcdir@/src/lib/python:@abs_top_builddir@/src/lib/python:@abs_top_builddir@/src/lib/dns/.libs:@abs_top_builddir@/src/lib/xfr/.libs
 export PYTHONPATH
 
 B10_FROM_SOURCE=@abs_top_srcdir@

+ 134 - 0
src/bin/bind10/tests/args_test.py

@@ -0,0 +1,134 @@
+"""
+This program tests the boss process to make sure that it runs while
+dropping permissions. It must be run as a user that can set permission.
+"""
+import unittest
+import os
+import sys
+import subprocess
+import select
+import time
+import pwd
+
+# Set to a valid user name on the system to run setuid() test
+#SUID_USER=None
+SUID_USER="shane"
+
+BIND10_EXE="../run_bind10.sh"
+TIMEOUT=3
+
+class TestBossArgs(unittest.TestCase):
+    def _waitForString(self, bob, s):
+        found_string = False
+        start_time = time.time()
+        while time.time() < start_time + TIMEOUT:
+            (r,w,x) = select.select((bob.stdout,), (), (), TIMEOUT) 
+            if bob.stdout in r:
+                s = bob.stdout.readline()
+                if s == '':
+                    break
+                if s.startswith(s): 
+                    found_string = True
+                    break
+        return found_string
+
+    def testNoArgs(self):
+        """Run bind10 without any arguments"""
+        bob = subprocess.Popen(args=(BIND10_EXE,),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        started_ok = self._waitForString(bob, '[bind10] BIND 10 started')
+        time.sleep(0.1)
+        bob.terminate()
+        bob.wait()
+        self.assertTrue(started_ok)
+
+    def testBadOption(self):
+        """Run bind10 with a bogus option"""
+        bob = subprocess.Popen(args=(BIND10_EXE, "--badoption"),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        failed = self._waitForString(bob, 'bind10: error: no such option: --badoption')
+        time.sleep(0.1)
+        bob.terminate()
+        self.assertTrue(bob.wait() == 2)
+        self.assertTrue(failed)
+
+    def testArgument(self):
+        """Run bind10 with an argument (this is not allowed)"""
+        bob = subprocess.Popen(args=(BIND10_EXE, "argument"),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        failed = self._waitForString(bob, 'Usage: bind10 [options]')
+        time.sleep(0.1)
+        bob.terminate()
+        self.assertTrue(bob.wait() == 1)
+        self.assertTrue(failed)
+
+    def testBadUser(self):
+        """Run bind10 with a bogus user"""
+        bob = subprocess.Popen(args=(BIND10_EXE, "-u", "bogus_user"),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        failed = self._waitForString(bob, "bind10: invalid user: 'bogus_user'")
+        time.sleep(0.1)
+        bob.terminate()
+        self.assertTrue(bob.wait() == 1)
+        self.assertTrue(failed)
+
+    def testBadUid(self):
+        """Run bind10 with a bogus user ID"""
+        bob = subprocess.Popen(args=(BIND10_EXE, "-u", "999999999"),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        failed = self._waitForString(bob, "bind10: invalid user: '999999999'")
+        time.sleep(0.1)
+        bob.terminate()
+        self.assertTrue(bob.wait() == 1)
+        self.assertTrue(failed)
+
+    def testFailSetUser(self):
+        """Try the -u option when we don't run as root"""
+        global SUID_USER
+        if SUID_USER is None:
+            self.skipTest("test needs a valid user (set when run)")
+        if os.getuid() == 0:
+            self.skipTest("test must not be run as root (uid is 0)")
+        # XXX: we depend on the "nobody" user
+        bob = subprocess.Popen(args=(BIND10_EXE, "-u", "nobody"),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        failed = self._waitForString(bob, "[bind10] Error on startup: Unable to start b10-msgq; Unable to change to user nobody")
+        time.sleep(0.1)
+        bob.terminate()
+        self.assertTrue(bob.wait() == 1)
+        self.assertTrue(failed)
+
+    def testSetUser(self):
+        """Try the -u option"""
+        global SUID_USER
+        if SUID_USER is None:
+            self.skipTest("test needs a valid user (set when run)")
+        if os.getuid() != 0:
+            self.skipTest("test must run as root (uid is not 0)")
+        if os.geteuid() != 0:
+            self.skipTest("test must run as root (euid is not 0)")
+
+        bob = subprocess.Popen(args=(BIND10_EXE, "-u", SUID_USER),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        started_ok = self._waitForString(bob, '[bind10] BIND 10 started')
+        self.assertTrue(started_ok)
+        ps = subprocess.Popen(args=("ps", "axo", "user,pid"),
+                              stdout=subprocess.PIPE)
+        s = ps.stdout.readline()
+        ps_user = None
+        while True:
+            s = ps.stdout.readline()
+            if s == '': break
+            (user, pid) = s.split()
+            if int(pid) == bob.pid:
+                ps_user = user.decode()
+                break
+        self.assertTrue(ps_user is not None)
+        self.assertTrue(ps_user == SUID_USER)
+        time.sleep(0.1)
+        bob.terminate()
+        x = bob.wait()
+        self.assertTrue(bob.wait() == 0)
+
+if __name__ == '__main__':
+    unittest.main()

+ 2 - 1
src/bin/bind10/tests/bind10_test.in

@@ -27,5 +27,6 @@ PYTHONPATH=@abs_top_srcdir@/src/lib/python:@abs_top_srcdir@/src/bin/bind10
 export PYTHONPATH
 
 cd ${BIND10_PATH}/tests
-exec ${PYTHON_EXEC} -O bind10_test.py $*
+${PYTHON_EXEC} -O bind10_test.py $*
+exec ${PYTHON_EXEC} -O args_test.py $*
 

+ 0 - 13
src/bin/bindctl/Makefile.am

@@ -9,8 +9,6 @@ python_PYTHON = __init__.py bindcmd.py cmdparse.py exception.py moduleinfo.py my
 pythondir = $(pyexecdir)/bindctl
 
 bindctldir = $(DESTDIR)$(pkgdatadir)
-bindctl_DATA = bindctl.pem
-EXTRA_DIST += bindctl.pem
 
 CLEANFILES = bindctl
 
@@ -26,14 +24,3 @@ bindctl: bindctl-source.py
 	       -e "s|@@SYSCONFDIR@@|@sysconfdir@|" \
 	       -e "s|@@LIBEXECDIR@@|$(pkglibexecdir)|" bindctl-source.py >$@
 	chmod a+x $@
-
-if INSTALL_CONFIGURATIONS
-
-# TODO: permissions handled later
-install-data-local:
-	$(mkinstalldirs) $(DESTDIR)/@sysconfdir@/@PACKAGE@   
-	if test ! -f $(DESTDIR)$(sysconfdir)/@PACKAGE@/bindctl.pem; then	\
-	  $(INSTALL_DATA) $(srcdir)/bindctl.pem $(DESTDIR)$(sysconfdir)/@PACKAGE@/ ;	\
-	fi
-
-endif

+ 1 - 0
src/bin/bindctl/TODO

@@ -18,3 +18,4 @@ functions should be updated first.):
    If the default user is saved in file, its password shouldn't be saved in plaintext.
 7. Need to think of what exactly to do with responses received to commands
    (currently it simply print the map)
+8. Remove bindctl-source.py.in(replace with bindctl-source.py) when merging to trunk.

+ 131 - 80
src/bin/bindctl/bindcmd.py

@@ -36,6 +36,9 @@ import getpass
 from hashlib import sha1
 import csv
 import ast
+import pwd
+import getpass
+import traceback
 
 try:
     from collections import OrderedDict
@@ -49,6 +52,8 @@ try:
 except ImportError:
     my_readline = sys.stdin.readline
 
+CSV_FILE_NAME = 'default_user.csv'
+FAIL_TO_CONNECT_WITH_CMDCTL = "Fail to connect with b10-cmdctl module, is it running?"
 CONFIG_MODULE_NAME = 'config'
 CONST_BINDCTL_HELP = """
 usage: <module name> <command name> [param1 = value1 [, param2 = value2]]
@@ -58,10 +63,34 @@ Type \"<module_name> help\" for help on the specific module.
 Type \"<module_name> <command_name> help\" for help on the specific command.
 \nAvailable module names: """
 
+class ValidatedHTTPSConnection(http.client.HTTPSConnection):
+    '''Overrides HTTPSConnection to support certification 
+    validation. '''
+    def __init__(self, host, ca_certs):
+        http.client.HTTPSConnection.__init__(self, host)
+        self.ca_certs = ca_certs
+
+    def connect(self):
+        ''' Overrides the connect() so that we do 
+        certificate validation. '''
+        sock = socket.create_connection((self.host, self.port),
+                                        self.timeout)
+        if self._tunnel_host:
+            self.sock = sock
+            self._tunnel()
+       
+        req_cert = ssl.CERT_NONE
+        if self.ca_certs:
+            req_cert = ssl.CERT_REQUIRED
+        self.sock = ssl.wrap_socket(sock, self.key_file,
+                                    self.cert_file,
+                                    cert_reqs=req_cert,
+                                    ca_certs=self.ca_certs)
+
 class BindCmdInterpreter(Cmd):
     """simple bindctl example."""    
 
-    def __init__(self, server_port = 'localhost:8080', pem_file = "bindctl.pem"):
+    def __init__(self, server_port = 'localhost:8080', pem_file = None):
         Cmd.__init__(self)
         self.location = ""
         self.prompt_end = '> '
@@ -70,40 +99,73 @@ class BindCmdInterpreter(Cmd):
         self.modules = OrderedDict()
         self.add_module_info(ModuleInfo("help", desc = "Get help for bindctl"))
         self.server_port = server_port
-        self.pem_file = pem_file
-        self._connect_to_cmd_ctrld()
+        self.conn = ValidatedHTTPSConnection(self.server_port,
+                                             ca_certs=pem_file)
         self.session_id = self._get_session_id()
 
-    def _connect_to_cmd_ctrld(self):
-        '''Connect to cmdctl in SSL context. '''
-        try:
-            self.conn = http.client.HTTPSConnection(self.server_port,
-                          cert_file=self.pem_file)
-        except  Exception as e:
-            print(e, "can't connect to %s, please make sure cmd-ctrld is running" %
-                  self.server_port)
-
     def _get_session_id(self):
         '''Generate one session id for the connection. '''
         rand = os.urandom(16)
         now = time.time()
-        session_id = sha1(("%s%s%s" %(rand, now, 
+        session_id = sha1(("%s%s%s" %(rand, now,
                                       socket.gethostname())).encode())
         digest = session_id.hexdigest()
         return digest
     
     def run(self):
-        '''Parse commands inputted from user and send them to cmdctl. '''
+        '''Parse commands from user and send them to cmdctl. '''
         try:
             if not self.login_to_cmdctl():
-                return False
+                return 
 
-            # Get all module information from cmd-ctrld
-            self.config_data = isc.config.UIModuleCCSession(self)
-            self._update_commands()
             self.cmdloop()
+        except FailToLogin as err:
+            print(err)
+            print(FAIL_TO_CONNECT_WITH_CMDCTL)
+            traceback.print_exc()
         except KeyboardInterrupt:
-            return True
+            print('\nExit from bindctl')
+
+    def _get_saved_user_info(self, dir, file_name):
+        ''' Read all the available username and password pairs saved in 
+        file(path is "dir + file_name"), Return value is one list of elements
+        ['name', 'password'], If get information failed, empty list will be 
+        returned.'''
+        if (not dir) or (not os.path.exists(dir)):
+            return []
+
+        try:
+            csvfile = None
+            users = []
+            csvfile = open(dir + file_name)
+            users_info = csv.reader(csvfile)
+            for row in users_info:
+                users.append([row[0], row[1]])
+        except (IOError, IndexError) as e:
+            pass
+        finally:
+            if csvfile:
+                csvfile.close()
+            return users
+
+    def _save_user_info(self, username, passwd, dir, file_name):
+        ''' Save username and password in file "dir + file_name"
+        If it's saved properly, return True, or else return False. '''
+        try:
+            if not os.path.exists(dir):
+                os.mkdir(dir, 0o700)
+
+            csvfilepath = dir + file_name 
+            csvfile = open(csvfilepath, 'w')
+            os.chmod(csvfilepath, 0o600)
+            writer = csv.writer(csvfile)
+            writer.writerow([username, passwd])
+            csvfile.close()
+        except Exception as e:
+            print(e, "\nCannot write %s%s; default user is not stored" % (dir, file_name))
+            return False
+
+        return True
 
     def login_to_cmdctl(self):
         '''Login to cmdctl with the username and password inputted 
@@ -112,33 +174,21 @@ class BindCmdInterpreter(Cmd):
         time, username and password saved in 'default_user.csv' will be
         used first.
         '''
-        csvfile = None
-        bsuccess = False
-        try:
-            cvsfilepath = ""
-            if ('HOME' in os.environ):
-                cvsfilepath = os.environ['HOME']
-                cvsfilepath += os.sep + '.bind10' + os.sep
-            cvsfilepath += 'default_user.csv'
-            csvfile = open(cvsfilepath)
-            users = csv.reader(csvfile)
-            for row in users:
-                param = {'username': row[0], 'password' : row[1]}
+        csv_file_dir = pwd.getpwnam(getpass.getuser()).pw_dir
+        csv_file_dir += os.sep + '.bind10' + os.sep
+        users = self._get_saved_user_info(csv_file_dir, CSV_FILE_NAME)
+        for row in users:
+            param = {'username': row[0], 'password' : row[1]}
+            try:
                 response = self.send_POST('/login', param)
                 data = response.read().decode()
-                if response.status == http.client.OK:
-                    print(data + ' login as ' + row[0] )
-                    bsuccess = True
-                    break
-        except IOError as e:
-            pass
-        except Exception as e:
-            print(e)
-        finally:
-            if csvfile:
-                csvfile.close()
-            if bsuccess:
-                return True
+            except socket.error:
+                traceback.print_exc()
+                raise FailToLogin()
+
+            if response.status == http.client.OK:
+                print(data + ' login as ' + row[0] )
+                return True 
 
         count = 0
         print("[TEMP MESSAGE]: username :root  password :bind10")
@@ -151,34 +201,18 @@ class BindCmdInterpreter(Cmd):
             username = input("Username:")
             passwd = getpass.getpass()
             param = {'username': username, 'password' : passwd}
-            response = self.send_POST('/login', param)
-            data = response.read().decode()
-            print(data)
-            
+            try:
+                response = self.send_POST('/login', param)
+                data = response.read().decode()
+                print(data)
+            except socket.error as e:
+                traceback.print_exc()
+                raise FailToLogin()
+
             if response.status == http.client.OK:
-                cvsfilepath = ""
-                try:
-                    if ('HOME' in os.environ):
-                        cvsfilepath = os.environ['HOME']
-                        cvsfilepath += os.sep + '.bind10' + os.sep
-                        if not os.path.exists(cvsfilepath):
-                                os.mkdir(cvsfilepath, 0o700)
-                    else:
-                        print("Cannot determine location of $HOME. Not storing default user")
-                        return True
-                    cvsfilepath += 'default_user.csv'
-                    csvfile = open(cvsfilepath, 'w')
-                    os.chmod(cvsfilepath, 0o600)
-                    writer = csv.writer(csvfile)
-                    writer.writerow([username, passwd])
-                    csvfile.close()
-                except Exception as e:
-                    # just not store it
-                    print("Cannot write ~/.bind10/default_user.csv; default user is not stored")
-                    print(e)
+                self._save_user_info(username, passwd, csv_file_dir, CSV_FILE_NAME)
                 return True
 
-
     def _update_commands(self):
         '''Update the commands of all modules. '''
         for module_name in self.config_data.get_config_item_list():
@@ -211,7 +245,19 @@ class BindCmdInterpreter(Cmd):
         headers = {"cookie" : self.session_id}
         self.conn.request('POST', url, param, headers)
         return self.conn.getresponse()
-        
+
+    def _update_all_modules_info(self):
+        ''' Get all modules' information from cmdctl, including
+        specification file and configuration data. This function
+        should be called before interpreting command line or complete-key
+        is entered. This may not be the best way to keep bindctl
+        and cmdctl share same modules information, but it works.'''
+        self.config_data = isc.config.UIModuleCCSession(self)
+        self._update_commands()
+
+    def precmd(self, line):
+        self._update_all_modules_info()
+        return line 
 
     def postcmd(self, stop, line):
         '''Update the prompt after every command'''
@@ -308,8 +354,11 @@ class BindCmdInterpreter(Cmd):
         if cmd.module != CONFIG_MODULE_NAME:
             for param_name in cmd.params:
                 param_spec = command_info.get_param_with_name(param_name).param_spec
-                cmd.params[param_name] = isc.config.config_data.convert_type(param_spec, cmd.params[param_name])
-
+                try:
+                    cmd.params[param_name] = isc.config.config_data.convert_type(param_spec, cmd.params[param_name])
+                except isc.cc.data.DataTypeError as e:
+                    raise isc.cc.data.DataTypeError('Invalid parameter value for \"%s\", the type should be \"%s\" \n' 
+                                                     % (param_name, param_spec['item_type']) + str(e))
     
     def _handle_cmd(self, cmd):
         '''Handle a command entered by the user'''
@@ -356,6 +405,7 @@ class BindCmdInterpreter(Cmd):
 
     def complete(self, text, state):
         if 0 == state:
+            self._update_all_modules_info()
             text = text.strip()
             hints = []
             cur_line = my_readline()
@@ -441,13 +491,14 @@ class BindCmdInterpreter(Cmd):
             cmd = BindCmdParse(line)
             self._validate_cmd(cmd)
             self._handle_cmd(cmd)
-        except BindCtlException as e:
-            print("Error! ", e)
-            self._print_correct_usage(e)
-        except isc.cc.data.DataTypeError as e:
-            print("Error! ", e)
-            self._print_correct_usage(e)
-            
+        except (IOError, http.client.HTTPException) as err:
+            print('Error!', err)
+            print(FAIL_TO_CONNECT_WITH_CMDCTL)
+        except BindCtlException as err:
+            print("Error! ", err)
+            self._print_correct_usage(err)
+        except isc.cc.data.DataTypeError as err:
+            print("Error! ", err)
             
     def _print_correct_usage(self, ept):        
         if isinstance(ept, CmdUnknownModuleSyntaxError):
@@ -556,7 +607,7 @@ class BindCmdInterpreter(Cmd):
         if (len(cmd.params) != 0):
             cmd_params = json.dumps(cmd.params)
 
-        print("send the message to cmd-ctrld")        
+        print("send the command to cmd-ctrld")        
         reply = self.send_POST(url, cmd.params)
         data = reply.read().decode()
         print("received reply:", data)

+ 8 - 12
src/bin/bindctl/bindctl-source.py.in

@@ -97,13 +97,16 @@ def check_addr(option, opt_str, value, parser):
 
 def set_bindctl_options(parser):
     parser.add_option('-p', '--port', dest = 'port', type = 'int',
-            action = 'callback', callback=check_port,
-            default = '8080', help = 'port for cmdctl of bind10')
+                      action = 'callback', callback=check_port,
+                      default = '8080', help = 'port for cmdctl of bind10')
 
     parser.add_option('-a', '--address', dest = 'addr', type = 'string',
-            action = 'callback', callback=check_addr,
-            default = '127.0.0.1', help = 'IP address for cmdctl of bind10')
+                      action = 'callback', callback=check_addr,
+                      default = '127.0.0.1', help = 'IP address for cmdctl of bind10')
 
+    parser.add_option('-c', '--certificate-chain', dest = 'cert_chain', 
+                      type = 'string', action = 'store',
+                      help = 'PEM formatted server certificate validation chain file')
 
 if __name__ == '__main__':
     try:
@@ -111,14 +114,7 @@ if __name__ == '__main__':
         set_bindctl_options(parser)
         (options, args) = parser.parse_args()
         server_addr = options.addr + ':' + str(options.port)
-        # If B10_FROM_SOURCE is set in the environment, we use PEM file
-        # from a directory relative to that, otherwise we use the one
-        # installed on the system
-        if "B10_FROM_SOURCE" in os.environ:
-            SYSCONF_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/bindctl"
-        else:
-            SYSCONF_PATH = "@@SYSCONFDIR@@/@PACKAGE@"
-        tool = BindCmdInterpreter(server_addr, pem_file = SYSCONF_PATH + "/bindctl.pem")
+        tool = BindCmdInterpreter(server_addr, pem_file=options.cert_chain)
         prepare_config_commands(tool)
         tool.run()
     except Exception as e:

+ 0 - 36
src/bin/bindctl/bindctl.pem

@@ -1,36 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIICXAIBAAKBgQDpICWxJGKMvUhLFPbf5n8ZWogqjYcQqqoHqHVRHYjyiey6FZdt
-ZkY2s1gYh0G0NXtimlIgic+vEcFe7vdmyKntW7DYDaqAj0KrED7RKAj8324jNbSJ
-HtLP4evvJep3vsoNtTvNuceQJ46vukxyxgg3DuC9kVqPuD8CZ1Rq4ATyiwIDAQAB
-AoGBAOJlOtV+DUq6Y2Ou91VXRiU8GzKgAQP5iWgoe84Ljbxkn4XThBxVD2j94Fbp
-u7AjpDCMx6cbzpoo9w6XqaGizAmAehIfTE3eFYs74N/FM09Wg2OSDyxMY0jgyECU
-A4ukjlPwcGDbmgbmlY3i+FVHp+zCgtZEsMC1IAosMac1BoX5AkEA/lrXWaVtH8bo
-mut3GBaXvubZMdaUr0BUd5a9q+tt4dQcKG1kFqgCNKhNhBIcpiMVcz+jGmOuopNA
-8dnUGqv3FQJBAOqiJ54ZvOTWNDpJIe02wIXRxRmc1xhHFCqYP23KxBVrAcTYB19J
-lesov/hEbnGLCbKS/naZJ1zrTImUPNRLqx8CQCzDtA7U7GWhTiKluioFH+O7IRKC
-X1yQh80cPHlbT9VkzSfYSLssCmdWD35k6aHbntTPqFbmoD+AhveJjKi9BxkCQDwX
-1c+/RcrSNcQr0N2hZUOgyztZGRnlsnuKTMyA3yGhK23P6mt0PEpjQG+Ej0jTVGOB
-FF0pspQwy4R9C+tPif8CQH36NNlXBfVNmT7kDtyLmaE6pID0vY9duX56BJbU1R0x
-SQ8/LcfJagk6gvp08OyYCPA+WZ7u/bas9R/nMTCLivc=
------END RSA PRIVATE KEY-----
------BEGIN CERTIFICATE-----
-MIIDhzCCAvCgAwIBAgIJALwngNFik7ONMA0GCSqGSIb3DQEBBQUAMIGKMQswCQYD
-VQQGEwJjbjEQMA4GA1UECBMHYmVpamluZzEQMA4GA1UEBxMHYmVpamluZzEOMAwG
-A1UEChMFY25uaWMxDjAMBgNVBAsTBWNubmljMRMwEQYDVQQDEwp6aGFuZ2xpa3Vu
-MSIwIAYJKoZIhvcNAQkBFhN6aGFuZ2xpa3VuQGNubmljLmNuMB4XDTEwMDEwNzEy
-NDcxOFoXDTExMDEwNzEyNDcxOFowgYoxCzAJBgNVBAYTAmNuMRAwDgYDVQQIEwdi
-ZWlqaW5nMRAwDgYDVQQHEwdiZWlqaW5nMQ4wDAYDVQQKEwVjbm5pYzEOMAwGA1UE
-CxMFY25uaWMxEzARBgNVBAMTCnpoYW5nbGlrdW4xIjAgBgkqhkiG9w0BCQEWE3po
-YW5nbGlrdW5AY25uaWMuY24wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOkg
-JbEkYoy9SEsU9t/mfxlaiCqNhxCqqgeodVEdiPKJ7LoVl21mRjazWBiHQbQ1e2Ka
-UiCJz68RwV7u92bIqe1bsNgNqoCPQqsQPtEoCPzfbiM1tIke0s/h6+8l6ne+yg21
-O825x5Anjq+6THLGCDcO4L2RWo+4PwJnVGrgBPKLAgMBAAGjgfIwge8wHQYDVR0O
-BBYEFJKM/O0ViGlwtb3JEci/DLTO/7DaMIG/BgNVHSMEgbcwgbSAFJKM/O0ViGlw
-tb3JEci/DLTO/7DaoYGQpIGNMIGKMQswCQYDVQQGEwJjbjEQMA4GA1UECBMHYmVp
-amluZzEQMA4GA1UEBxMHYmVpamluZzEOMAwGA1UEChMFY25uaWMxDjAMBgNVBAsT
-BWNubmljMRMwEQYDVQQDEwp6aGFuZ2xpa3VuMSIwIAYJKoZIhvcNAQkBFhN6aGFu
-Z2xpa3VuQGNubmljLmNuggkAvCeA0WKTs40wDAYDVR0TBAUwAwEB/zANBgkqhkiG
-9w0BAQUFAAOBgQBh5N6isMAQAFFD+pbfpppjQlO4vUNcEdzPdeuBFaf9CsX5ZdxV
-jmn1ZuGm6kRzqUPwPSxvCIAY0wuSu1g7YREPAZ3XBVwcg6262iGOA6n7E+nv5PLz
-EuZ1oUg+IfykUIoflKH6xZB4MyPL+EgkMT+i9BrngaXHXF8tEO30YppMiA==
------END CERTIFICATE-----

+ 8 - 5
src/bin/bindctl/cmdparse.py

@@ -24,15 +24,19 @@ except ImportError:
     from bindctl.mycollections import OrderedDict
 
 param_name_str = "^\s*(?P<param_name>[\w]+)\s*=\s*"
-param_value_str = "(?P<param_value>[\w\.:/-]+)"
-param_value_with_quota_str = "[\"\'](?P<param_value>[\w\.:, /-]+)[\"\']"
+
+# The value string can be a sequence without space or comma 
+# characters, or a string surroundedby quotation marks(such marks
+# can be part of string in an escaped form)
+#param_value_str  = "(?P<param_value>[\"\'].+?(?<!\\\)[\"\']|[^\'\"][^, ]+)"
+param_value_str  = "(?P<param_value>[^\'\" ][^, ]+)"
+param_value_with_quota_str  = "[\"\'](?P<param_value>.+?)(?<!\\\)[\"\']"
 next_params_str = "(?P<blank>\s*)(?P<comma>,?)(?P<next_params>.*)$"
 
 PARAM_WITH_QUOTA_PATTERN = re.compile(param_name_str + 
-                                      param_value_with_quota_str +
+                                      param_value_with_quota_str + 
                                       next_params_str)
 PARAM_PATTERN = re.compile(param_name_str + param_value_str + next_params_str)
-                           
 # Used for module and command name
 NAME_PATTERN = re.compile("^\s*(?P<name>[\w]+)(?P<blank>\s*)(?P<others>.*)$")
 
@@ -98,7 +102,6 @@ class BindCmdParse:
                 
             groups = PARAM_PATTERN.match(param_text) or \
                      PARAM_WITH_QUOTA_PATTERN.match(param_text)
-            
             if not groups:
                 # ok, fill in the params in the order entered
                 params = re.findall("([^\" ]+|\".*\")", param_text)

+ 7 - 0
src/bin/bindctl/exception.py

@@ -115,3 +115,10 @@ class CmdMissParamSyntaxError(CmdSyntaxError):
     def __str__(self):
         return str("Parameter '%s' is missed for command '%s' of module '%s'" % 
                    (self.param, self.command, self.module))
+
+
+class FailToLogin(BindCtlException):
+    def __str__(self):
+        return "Fail to login to cmdctl"
+
+

+ 48 - 4
src/bin/bindctl/tests/bindctl_test.py

@@ -16,6 +16,7 @@
 
 import unittest
 import isc.cc.data
+import os
 from bindctl import cmdparse
 from bindctl import bindcmd
 from bindctl.moduleinfo import *
@@ -50,12 +51,31 @@ class TestCmdLex(unittest.TestCase):
             assert cmd.params["zone_name"] == "cnnic.cn"
             assert cmd.params["file"] == "cnnic.cn.file"
             assert cmd.params["master"] == '1.1.1.1'
+
+    def testCommandWithParamters_2(self):
+        '''Test whether the parameters in key=value can be parsed properly.'''
+        cmd = cmdparse.BindCmdParse('zone cmd name = 1:34::2')
+        self.assertEqual(cmd.params['name'], '1:34::2')
+
+        cmd = cmdparse.BindCmdParse('zone cmd name = 1\"\'34**&2 value=44\"\'\"')
+        self.assertEqual(cmd.params['name'], '1\"\'34**&2')
+        self.assertEqual(cmd.params['value'], '44\"\'\"')
+
+        cmd = cmdparse.BindCmdParse('zone cmd name = 1\"\'34**&2 ,value=  44\"\'\"')
+        self.assertEqual(cmd.params['name'], '1\"\'34**&2')
+        self.assertEqual(cmd.params['value'], '44\"\'\"')
             
+        cmd = cmdparse.BindCmdParse('zone cmd name =  1\'34**&2value=44\"\'\" value = \"==============\'')
+        self.assertEqual(cmd.params['name'], '1\'34**&2value=44\"\'\"')
+        self.assertEqual(cmd.params['value'], '==============')
+
+        cmd = cmdparse.BindCmdParse('zone cmd name =    \"1234, 567890 \" value ==&*/')
+        self.assertEqual(cmd.params['name'], '1234, 567890 ')
+        self.assertEqual(cmd.params['value'], '=&*/')
             
     def testCommandWithListParam(self):
-            cmd = cmdparse.BindCmdParse("zone set zone_name='cnnic.cn', master='1.1.1.1, 2.2.2.2'")
-            assert cmd.params["master"] == '1.1.1.1, 2.2.2.2'            
-        
+        cmd = cmdparse.BindCmdParse("zone set zone_name='cnnic.cn', master='1.1.1.1, 2.2.2.2'")
+        assert cmd.params["master"] == '1.1.1.1, 2.2.2.2'            
         
     def testCommandWithHelpParam(self):
         cmd = cmdparse.BindCmdParse("zone add help")
@@ -217,8 +237,32 @@ class TestNameSequence(unittest.TestCase):
             assert self.random_names[i] == cmd_names[i+1]
             assert self.random_names[i] == module_names[i+1]
             i = i + 1
-        
     
+class FakeBindCmdInterpreter(bindcmd.BindCmdInterpreter):
+    def __init__(self):
+        pass
+
+class TestBindCmdInterpreter(unittest.TestCase):
+
+    def _create_invalid_csv_file(self, csvfilename):
+        import csv
+        csvfile = open(csvfilename, 'w')
+        writer = csv.writer(csvfile)
+        writer.writerow(['name1'])
+        writer.writerow(['name2'])
+        csvfile.close()
+
+    def test_get_saved_user_info(self):
+        cmd = FakeBindCmdInterpreter()
+        users = cmd._get_saved_user_info('/notexist', 'cvs_file.cvs')
+        self.assertEqual([], users)
+        
+        csvfilename = 'csv_file.csv'
+        self._create_invalid_csv_file(csvfilename)
+        users = cmd._get_saved_user_info('./', csvfilename)
+        self.assertEqual([], users)
+        os.remove(csvfilename)
+
 if __name__== "__main__":
     unittest.main()
     

+ 3 - 1
src/bin/cfgmgr/Makefile.am

@@ -1,8 +1,10 @@
+SUBDIRS = tests
+
 pkglibexecdir = $(libexecdir)/@PACKAGE@
 
 pkglibexec_SCRIPTS = b10-cfgmgr
 
-CLEANFILES = b10-cfgmgr
+CLEANFILES = b10-cfgmgr b10-cfgmgr.pyc
 
 b10_cfgmgrdir = @localstatedir@/@PACKAGE@
 #B10_cfgmgr_DATA = 

+ 7 - 1
src/bin/cfgmgr/b10-cfgmgr.py.in

@@ -15,6 +15,8 @@
 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
+# $Id$
+
 import sys; sys.path.append ('@@PYTHONPATH@@')
 
 from isc.config.cfgmgr import ConfigManager
@@ -38,7 +40,8 @@ def signal_handler(signal, frame):
     if cm:
         cm.running = False
 
-if __name__ == "__main__":
+def main():
+    global cm
     try:
         cm = ConfigManager(DATA_PATH)
         signal.signal(signal.SIGINT, signal_handler)
@@ -53,3 +56,6 @@ if __name__ == "__main__":
         print("[b10-cfgmgr] Interrupted, exiting")
     if cm:
         cm.write_config()
+
+if __name__ == "__main__":
+    main()

+ 13 - 0
src/bin/cfgmgr/tests/Makefile.am

@@ -0,0 +1,13 @@
+PYTESTS = b10-cfgmgr_test.py
+
+EXTRA_DIST = $(PYTESTS)
+
+# later will have configure option to choose this, like: coverage run --branch
+PYCOVERAGE = $(PYTHON)
+# test using command-line arguments, so use check-local target instead of TESTS
+check-local:
+	for pytest in $(PYTESTS) ; do \
+	echo Running test: $$pytest ; \
+	env PYTHONPATH=$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python:$(abs_top_builddir)/src/bin/cfgmgr \
+	$(PYCOVERAGE) $(abs_builddir)/$$pytest ; \
+	done

+ 95 - 0
src/bin/cfgmgr/tests/b10-cfgmgr_test.py.in

@@ -0,0 +1,95 @@
+# Copyright (C) 2010  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+# $Id: cfgmgr_test.py 2126 2010-06-16 14:40:22Z jelte $
+
+#
+# Tests for the configuration manager run script
+#
+
+import unittest
+import os
+import sys
+
+class MyConfigManager:
+    def __init__(self, path):
+        self._path = path
+        self.read_config_called = False
+        self.notify_boss_called = False
+        self.run_called = False
+        self.write_config_called = False
+        self.running = True
+
+    def read_config(self):
+        self.read_config_called = True
+
+    def notify_boss(self):
+        self.notify_boss_called = True
+
+    def run(self):
+        self.run_called = True
+
+    def write_config(self):
+        self.write_config_called = True
+
+class TestConfigManagerStartup(unittest.TestCase):
+    def test_cfgmgr(self):
+        # some creative module use;
+        # b10-cfgmgr has a hypen, so we use __import__
+        # this also gives us the chance to override the imported
+        # module ConfigManager in it.
+        b = __import__("b10-cfgmgr")
+        b.ConfigManager = MyConfigManager
+
+        b.main()
+
+        self.assertTrue(b.cm.read_config_called)
+        self.assertTrue(b.cm.notify_boss_called)
+        self.assertTrue(b.cm.run_called)
+        self.assertTrue(b.cm.write_config_called)
+
+        self.assertTrue(b.cm.running)
+        b.signal_handler(None, None)
+        self.assertFalse(b.cm.running)
+
+        # TODO: take value from the 'global config module'
+        # (and rename the .in away from this file again)
+        data_path = "@localstatedir@/@PACKAGE@".replace("${prefix}", "@prefix@")
+        self.assertEqual(data_path, b.DATA_PATH)
+
+        # remove the 'module' again, or later tests may fail
+        # (if it is already present it won't be loaded again)
+        sys.modules.pop("b10-cfgmgr")
+
+    def test_cfgmgr_from_source(self):
+        tmp_env_var = "/just/some/dir"
+        env_var = None
+        if "B10_FROM_SOURCE" in os.environ:
+            env_var = os.environ["B10_FROM_SOURCE"]
+
+        os.environ["B10_FROM_SOURCE"] = tmp_env_var
+        b = __import__("b10-cfgmgr", globals(), locals())
+        b.ConfigManager = MyConfigManager
+        self.assertEqual(tmp_env_var, b.DATA_PATH)
+
+        if env_var != None:
+            os.environ["B10_FROM_SOURCE"] = env_var
+
+        sys.modules.pop("b10-cfgmgr")
+
+
+if __name__ == '__main__':
+    unittest.main()
+

+ 4 - 2
src/bin/cmdctl/Makefile.am

@@ -17,9 +17,8 @@ b10_cmdctl_DATA = $(CMDCTL_CONFIGURATIONS)
 b10_cmdctl_DATA += cmdctl.spec
  
 EXTRA_DIST = $(CMDCTL_CONFIGURATIONS)
-EXTRA_DIST += cmdctl.spec
 
-CLEANFILES=	b10-cmdctl cmdctl.pyc
+CLEANFILES=	b10-cmdctl cmdctl.pyc cmdctl.spec
 
 man_MANS = b10-cmdctl.8
 EXTRA_DIST += $(man_MANS) b10-cmdctl.xml
@@ -31,6 +30,9 @@ b10-cmdctl.8: b10-cmdctl.xml
 
 endif
 
+cmdctl.spec: cmdctl.spec.pre
+	$(SED) -e "s|@@SYSCONFDIR@@|$(sysconfdir)|" cmdctl.spec.pre >$@
+
 # TODO: does this need $$(DESTDIR) also?
 # this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix
 b10-cmdctl: cmdctl.py

+ 3 - 5
src/bin/cmdctl/TODO

@@ -1,8 +1,6 @@
-. Refine code for b10-cmdctl.
-. Add value type check according module specification.
 . Add return code for RESTful API document of b10-cmdctl.
-. Add more unit tests for b10-cmdctl.
 . Update man page for b10-cmdctl?
+. Add check for the content of key/certificate file
+  (when cmdctl starts or is configured by bindctl).
+. Use only one msgq/session to communicate with other modules?
 
-. Add id to each command, so the receiver knows if the response is what it wants.
-. Make cmdctl can be configured through bindctl.(after id is added)

+ 269 - 137
src/bin/cmdctl/cmdctl.py.in

@@ -41,6 +41,7 @@ import csv
 import random
 import time
 import signal
+from isc.config import ccsession
 from optparse import OptionParser, OptionValueError
 from hashlib import sha1
 try:
@@ -50,29 +51,27 @@ except ImportError:
 
 __version__ = 'BIND10'
 URL_PATTERN = re.compile('/([\w]+)(?:/([\w]+))?/?')
+CONFIG_DATA_URL = 'config_data'
+MODULE_SPEC_URL = 'module_spec'
 
-# If B10_FROM_SOURCE is set in the environment, we use data files
+
+# 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_SOURCE" in os.environ:
-    SPECFILE_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/cmdctl"
-    SYSCONF_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/cmdctl"
+if "B10_FROM_BUILD" in os.environ:
+    SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/cmdctl"
 else:
     PREFIX = "@prefix@"
     DATAROOTDIR = "@datarootdir@"
     SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
-    SYSCONF_PATH = "@sysconfdir@/@PACKAGE@".replace("${prefix}", PREFIX)
 SPECFILE_LOCATION = SPECFILE_PATH + os.sep + "cmdctl.spec"
-USER_INFO_FILE = SYSCONF_PATH + os.sep + "cmdctl-accounts.csv"
-PRIVATE_KEY_FILE = SYSCONF_PATH + os.sep + "cmdctl-keyfile.pem"
-CERTIFICATE_FILE = SYSCONF_PATH + os.sep + "cmdctl-certfile.pem"
-        
+
+class CmdctlException(Exception):
+    pass
+       
 class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
     '''https connection request handler.
-    Currently only GET and POST are supported.
-
-    '''
-
+    Currently only GET and POST are supported.  '''
     def do_GET(self):
         '''The client should send its session id in header with 
         the name 'cookie'
@@ -167,13 +166,13 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
         user_name = user_info.get('username')
         if not user_name:
             return False, ["need user name"]
-        if not self.server.user_infos.get(user_name):
+        if not self.server.get_user_info(user_name):
             return False, ["user doesn't exist"]
 
         user_pwd = user_info.get('password')
         if not user_pwd:
             return False, ["need password"]
-        local_info = self.server.user_infos.get(user_name)
+        local_info = self.server.get_user_info(user_name)
         pwd_hashval = sha1((user_pwd + local_info[1]).encode())
         if pwd_hashval.hexdigest() != local_info[0]:
             return False, ["password doesn't match"] 
@@ -197,9 +196,6 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
                 pass
 
         rcode, reply = self.server.send_command_to_module(mod, cmd, param)
-        if self.server._verbose:
-            print('[b10-cmdctl] Finish send message \'%s\' to module %s' % (cmd, mod))
-
         ret = http.client.OK
         if rcode != 0:
             ret = http.client.BAD_REQUEST
@@ -214,65 +210,167 @@ class CommandControl():
     '''Get all modules' config data/specification from configmanager.
     receive command from client and resend it to proper module.
     '''
-
-    def __init__(self, verbose = False):
+    def __init__(self, httpserver, verbose = False):
+        ''' httpserver: the http server which use the object of
+        CommandControl to communicate with other modules. '''
         self._verbose = verbose
-        self.cc = isc.cc.Session()
-        self.cc.group_subscribe('Cmd-Ctrld')
-        self.module_spec = self.get_module_specification()
-        self.config_data = self.get_config_data()
+        self._httpserver = httpserver
+        self._lock = threading.Lock()
+        self._setup_session()
+        self.modules_spec = self._get_modules_specification()
+        self._config_data = self._get_config_data_from_config_manager()
+        self._serving = True
+        self._start_msg_handle_thread()
+
+    def _setup_session(self):
+        '''Setup the session for receving the commands
+        sent from other modules. There are two sessions 
+        for cmdctl, one(self.module_cc) is used for receiving 
+        commands sent from other modules, another one (self._cc) 
+        is used to send the command from Bindctl or other tools 
+        to proper modules.''' 
+        self._cc = isc.cc.Session()
+        self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
+                                              self.config_handler,
+                                              self.command_handler)
+        self._module_name = self._module_cc.get_module_spec().get_module_name()
+        self._cmdctl_config_data = self._module_cc.get_full_config()
+        self._module_cc.start()
+    
+    def _accounts_file_check(self, filepath):
+        ''' Check whether the accounts file is valid, each row
+        should be a list with 3 items.'''
+        csvfile = None
+        errstr = None
+        try:
+            csvfile = open(filepath)
+            reader = csv.reader(csvfile)
+            for row in reader:
+                a = (row[0], row[1], row[2])
+        except (IOError, IndexError) as e:
+            errstr = 'Invalid accounts file: ' + str(e)
+        finally:
+            if csvfile:
+                csvfile.close()
 
+        return errstr
+
+    def _config_data_check(self, new_config):
+        ''' Check whether the new config data is valid or
+        not. '''
+        errstr = None
+        for key in new_config:
+            if key == 'version':
+                continue
+            elif key in ['key_file', 'cert_file']:
+                #TODO, only check whether the file exist,
+                # further check need to be done: eg. whether
+                # the private/certificate is valid.
+                path = new_config[key]
+                if not os.path.exists(path):
+                    errstr = "the file doesn't exist: " + path
+            elif key == 'accounts_file':
+                errstr = self._accounts_file_check(new_config[key])
+            else:
+                errstr = 'unknown config item: ' + key
+            
+            if errstr != None:
+                self.log_info('Fail to apply config data, ' + errstr) 
+                return ccsession.create_answer(1, errstr)
+
+        return ccsession.create_answer(0)
+
+    def config_handler(self, new_config):
+        answer = self._config_data_check(new_config)
+        rcode, val = ccsession.parse_answer(answer)
+        if rcode != 0:
+            return answer
+
+        with self._lock:
+            for key in new_config:
+                if key in self._cmdctl_config_data:
+                    self._cmdctl_config_data[key] = new_config[key]
+        return answer
+
+    def command_handler(self, command, args):
+        answer = ccsession.create_answer(0)
+        if command == ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE:
+            with self._lock:
+                self.modules_spec[args[0]] = args[1]
+
+        elif command == ccsession.COMMAND_SHUTDOWN:
+            #When cmdctl get 'shutdown' command from boss, 
+            #shutdown the outer httpserver.
+            self._httpserver.shutdown()
+            self._serving = False
+
+        elif command == 'print_settings':
+            answer = ccsession.create_answer(0, self._cmdctl_config_data)
+        else:
+            answer = ccsession.create_answer(1, 'unknown command: ' + command)
+
+        return answer
+
+    def _start_msg_handle_thread(self):
+        ''' Start one thread to handle received message from msgq.'''
+        td = threading.Thread(target=self._handle_msg_from_msgq)
+        td.daemon = True
+        td.start()
+
+    def _handle_msg_from_msgq(self):
+        '''Process all the received commands with module session. '''
+        while self._serving:
+            self._module_cc.check_command() 
+ 
     def _parse_command_result(self, rcode, reply):
         '''Ignore the error reason when command rcode isn't 0, '''
         if rcode != 0:
             return {}
         return reply
 
-    def get_config_data(self):
+    def _get_config_data_from_config_manager(self):
         '''Get config data for all modules from configmanager '''
-        rcode, reply = self.send_command('ConfigManager', isc.config.ccsession.COMMAND_GET_CONFIG)
+        rcode, reply = self.send_command('ConfigManager', ccsession.COMMAND_GET_CONFIG)
         return self._parse_command_result(rcode, reply)
 
-
-    def update_config_data(self, module_name, command_name):
+    def _update_config_data(self, module_name, command_name):
         '''Get lastest config data for all modules from configmanager '''
-        if module_name == 'ConfigManager' and command_name == isc.config.ccsession.COMMAND_SET_CONFIG:
-            self.config_data = self.get_config_data()
+        if module_name == 'ConfigManager' and command_name == ccsession.COMMAND_SET_CONFIG:
+            data = self._get_config_data_from_config_manager()
+            with self._lock:
+                self._config_data = data
 
-    def get_module_specification(self):
-        rcode, reply = self.send_command('ConfigManager', isc.config.ccsession.COMMAND_GET_MODULE_SPEC)
+    def get_config_data(self):
+        with self._lock:
+            data = self._config_data
+        return data
+
+    def get_modules_spec(self):
+        with self._lock:
+            spec = self.modules_spec
+        return spec
+
+    def _get_modules_specification(self):
+        '''Get all the modules' specification files. '''
+        rcode, reply = self.send_command('ConfigManager', ccsession.COMMAND_GET_MODULE_SPEC)
         return self._parse_command_result(rcode, reply)
 
-    def handle_recv_msg(self):
-        '''Handle received message, if 'shutdown' is received, return False'''
-        (message, env) = self.cc.group_recvmsg(True)
-        command, arg = isc.config.ccsession.parse_command(message)
-        while command:
-            if command == isc.config.ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE:
-                self.module_spec[arg[0]] = arg[1]
-            elif command == isc.config.ccsession.COMMAND_SHUTDOWN:
-                return False
-            (message, env) = self.cc.group_recvmsg(True)
-            command, arg = isc.config.ccsession.parse_command(message)
-        
-        return True
-    
     def send_command_with_check(self, module_name, command_name, params = None):
         '''Before send the command to modules, check if module_name, command_name
         parameters are legal according the spec file of the module.
-        Return rcode, dict.
+        Return rcode, dict. TODO, the rcode should be defined properly.
         rcode = 0: dict is the correct returned value.
         rcode > 0: dict is : { 'error' : 'error reason' }
         '''
-
         # core module ConfigManager does not have a specification file
         if module_name == 'ConfigManager':
             return self.send_command(module_name, command_name, params)
 
-        if module_name not in self.module_spec.keys():
+        specs = self.get_modules_spec()
+        if module_name not in specs.keys():
             return 1, {'error' : 'unknown module'}
        
-        spec_obj = isc.config.module_spec.ModuleSpec(self.module_spec[module_name], False)
+        spec_obj = isc.config.module_spec.ModuleSpec(specs[module_name], False)
         errors = []
         if not spec_obj.validate_command(command_name, params, errors):
             return 1, {'error': errors[0]}
@@ -281,123 +379,157 @@ class CommandControl():
 
     def send_command(self, module_name, command_name, params = None):
         '''Send the command from bindctl to proper module. '''
-        
-        errstr = 'no error'
+        errstr = 'unknown error'
         if self._verbose:
-            self.log_info('[b10-cmdctl] send command \'%s\' to %s\n' %(command_name, module_name))
-        try:
-            msg = isc.config.ccsession.create_command(command_name, params)
-            seq = self.cc.group_sendmsg(msg, module_name)
+            self.log_info("Begin send command '%s' to module '%s'" %(command_name, module_name))
+
+        if module_name == self._module_name:
+            # Process the command sent to cmdctl directly. 
+            answer = self.command_handler(command_name, params)
+        else:
+            msg = ccsession.create_command(command_name, params)
+            seq = self._cc.group_sendmsg(msg, module_name)
             #TODO, it may be blocked, msqg need to add a new interface waiting in timeout.
-            answer, env = self.cc.group_recvmsg(False, seq)
-            if answer:
-                try:
-                    rcode, arg = isc.config.ccsession.parse_answer(answer)
-                    if rcode == 0:
-                        self.update_config_data(module_name, command_name)
-                        if arg != None:
-                            return rcode, arg
-                        else:
-                            return rcode, {}
+            answer, env = self._cc.group_recvmsg(False, seq)
+
+        if self._verbose:
+            self.log_info("Finish send command '%s' to module '%s'" % (command_name, module_name))
+
+        if answer:
+            try:
+                rcode, arg = ccsession.parse_answer(answer)
+                if rcode == 0:
+                    self._update_config_data(module_name, command_name)
+                    if arg != None:
+                        return rcode, arg
                     else:
-                        # todo: exception
-                        errstr = str(answer['result'][1])
-                except isc.config.ccsession.ModuleCCSessionError as mcse:
-                    errstr = str("Error in ccsession answer:") + str(mcse)
-                    self.log_info(answer)
-        except Exception as e:
-            errstr = str(e)
-            self.log_info('\'%s\':[b10-cmdctl] fail send command \'%s\' to %s\n' % (e, command_name, module_name))
+                        return rcode, {}
+                else:
+                    # TODO: exception
+                    errstr = str(answer['result'][1])
+            except ccsession.ModuleCCSessionError as mcse:
+                errstr = str("Error in ccsession answer:") + str(mcse)
+                self.log_info(errstr)
         
         return 1, {'error': errstr}
     
     def log_info(self, msg):
         sys.stdout.write("[b10-cmdctl] %s\n" % str(msg))
 
+    def get_cmdctl_config_data(self):
+        ''' If running in source code tree, use keyfile, certificate
+        and user accounts file in source code. '''
+        if "B10_FROM_SOURCE" in os.environ:
+            sysconf_path = os.environ["B10_FROM_SOURCE"] + "/src/bin/cmdctl/"
+            accountsfile  = sysconf_path + "cmdctl-accounts.csv"
+            keyfile = sysconf_path + "cmdctl-keyfile.pem"
+            certfile = sysconf_path + "cmdctl-certfile.pem"
+            return (keyfile, certfile, accountsfile)
+
+        with self._lock:
+            keyfile = self._cmdctl_config_data.get('key_file')
+            certfile = self._cmdctl_config_data.get('cert_file')
+            accountsfile = self._cmdctl_config_data.get('accounts_file')
+
+        return (keyfile, certfile, accountsfile)
+
 class SecureHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
     '''Make the server address can be reused.'''
     allow_reuse_address = True
 
-    def __init__(self, server_address, RequestHandlerClass, idle_timeout = 1200, verbose = False):
+    def __init__(self, server_address, RequestHandlerClass, 
+                 CommandControlClass,
+                 idle_timeout = 1200, verbose = False):
         '''idle_timeout: the max idle time for login'''
         http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
         self.user_sessions = {}
         self.idle_timeout = idle_timeout
-        self.cmdctrl = CommandControl()
-        self.__is_shut_down = threading.Event()
-        self.__serving = False
+        self.cmdctl = CommandControlClass(self, verbose)
         self._verbose = verbose
-        self.user_infos = {}
-        self._read_user_info()
+        self._lock = threading.Lock()
+        self._user_infos = {}
+        self._accounts_file = None
+
+    def _create_user_info(self, accounts_file):
+        '''Read all user's name and its' salt, hashed password 
+        from accounts file.'''
+        if (self._accounts_file == accounts_file) and (len(self._user_infos) > 0): 
+            return
+
+        with self._lock:
+            self._user_infos = {}
+            csvfile = None
+            try:
+                csvfile = open(accounts_file)
+                reader = csv.reader(csvfile)
+                for row in reader:
+                    self._user_infos[row[0]] = [row[1], row[2]]
+            except (IOError, IndexError) as e:
+                self.log_info("Fail to read user database, %s" % e)                
+            finally:
+                if csvfile:
+                    csvfile.close()
+
+        self._accounts_file = accounts_file
+        if len(self._user_infos) == 0:
+            self.log_info("Fail to get user information, will deny any user")                
+         
+    def get_user_info(self, username):
+        '''Get user's salt and hashed string. If the user
+        doesn't exist, return None, or else, the list 
+        [salt, hashed password] will be returned.'''
+        with self._lock:
+            info = self._user_infos.get(username)
+        return info
 
-    def _read_user_info(self):
-        '''Read all user's name and its' password from csv file.'''
-        csvfile = None
-        try:
-            csvfile = open(USER_INFO_FILE)
-            reader = csv.reader(csvfile)
-            for row in reader:
-                self.user_infos[row[0]] = [row[1], row[2]]
-        except Exception as e:
-            self.log_info('[b10-cmdctl] Fail to read user information :\'%s\'\n' % e)                
-        finally:
-            if csvfile:
-                csvfile.close()
-        
     def save_user_session_id(self, session_id):
-        # Record user's id and login time.
+        ''' Record user's id and login time. '''
         self.user_sessions[session_id] = time.time()
         
-    def get_request(self):
-        '''Get client request socket and wrap it in SSL context. '''
-        newsocket, fromaddr = self.socket.accept()
+    def _check_key_and_cert(self, key, cert):
+        # TODO, check the content of key/certificate file 
+        if not os.path.exists(key):
+            raise CmdctlException("key file '%s' doesn't exist " % key)
+
+        if not os.path.exists(cert):
+            raise CmdctlException("certificate file '%s' doesn't exist " % cert)
+
+    def _wrap_socket_in_ssl_context(self, sock, key, cert):
         try:
-            connstream = ssl.wrap_socket(newsocket,
-                                     server_side = True,
-                                     certfile = CERTIFICATE_FILE,
-                                     keyfile = PRIVATE_KEY_FILE,
-                                     ssl_version = ssl.PROTOCOL_SSLv23)
-            return (connstream, fromaddr)
-        except ssl.SSLError as e :
-            self.log_info('[b10-cmdctl] deny client\'s invalid connection:\'%s\'\n' % e)
-            self.close_request(newsocket)
+            self._check_key_and_cert(key, cert)
+            ssl_sock = ssl.wrap_socket(sock,
+                                      server_side = True,
+                                      certfile = cert,
+                                      keyfile = key,
+                                      ssl_version = ssl.PROTOCOL_SSLv23)
+            return ssl_sock 
+        except (ssl.SSLError, CmdctlException) as err :
+            self.log_info("Deny client's connection because %s" % str(err))
+            self.close_request(sock)
             # raise socket error to finish the request
             raise socket.error
-            
+
+    def get_request(self):
+        '''Get client request socket and wrap it in SSL context. '''
+        key, cert, account_file = self.cmdctl.get_cmdctl_config_data()
+        self._create_user_info(account_file)
+        newsocket, fromaddr = self.socket.accept()
+        ssl_sock = self._wrap_socket_in_ssl_context(newsocket, key, cert)
+        return (ssl_sock, fromaddr)
 
     def get_reply_data_for_GET(self, id, module):
         '''Currently only support the following three url GET request '''
         rcode, reply = http.client.NO_CONTENT, []        
         if not module:
-            if id == 'config_data':
-               rcode, reply = http.client.OK, self.cmdctrl.config_data
-            elif id == 'module_spec':
-                rcode, reply = http.client.OK, self.cmdctrl.module_spec
+            if id == CONFIG_DATA_URL:
+                rcode, reply = http.client.OK, self.cmdctl.get_config_data()
+            elif id == MODULE_SPEC_URL:
+                rcode, reply = http.client.OK, self.cmdctl.get_modules_spec()
         
         return rcode, reply 
 
-        
-    def serve_forever(self, poll_interval = 0.5):
-        '''Start cmdctl as one tcp server. '''
-        self.__serving = True
-        self.__is_shut_down.clear()
-        while self.__serving:
-            if not self.cmdctrl.handle_recv_msg():
-                break
-
-            r, w, e = select.select([self], [], [], poll_interval)
-            if r:
-                self._handle_request_noblock()
-
-        self.__is_shut_down.set()
-    
-    def shutdown(self):
-        self.__serving = False
-        self.__is_shut_down.wait()
-
-
     def send_command_to_module(self, module_name, command_name, params):
-        return self.cmdctrl.send_command_with_check(module_name, command_name, params)
+        return self.cmdctl.send_command_with_check(module_name, command_name, params)
    
     def log_info(self, msg):
         sys.stdout.write("[b10-cmdctl] %s\n" % str(msg))
@@ -416,8 +548,9 @@ def set_signal_handler():
 def run(addr = 'localhost', port = 8080, idle_timeout = 1200, verbose = False):
     ''' Start cmdctl as one https server. '''
     if verbose:
-        sys.stdout.write("[b10-cmdctl] starting on :%s port:%d\n" %(addr, port))
-    httpd = SecureHTTPServer((addr, port), SecureHTTPRequestHandler, idle_timeout, verbose)
+        sys.stdout.write("[b10-cmdctl] starting on %s port:%d\n" %(addr, port))
+    httpd = SecureHTTPServer((addr, port), SecureHTTPRequestHandler, 
+                             CommandControl, idle_timeout, verbose)
     httpd.serve_forever()
 
 def check_port(option, opt_str, value, parser):
@@ -453,13 +586,12 @@ def set_cmd_options(parser):
     parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
             help="display more about what is going on")
 
-
 if __name__ == '__main__':
     try:
+        set_signal_handler()
         parser = OptionParser(version = __version__)
         set_cmd_options(parser)
         (options, args) = parser.parse_args()
-        set_signal_handler()
         run(options.addr, options.port, options.idle_timeout, options.verbose)
     except isc.cc.SessionError as se:
         sys.stderr.write("[b10-cmdctl] Error creating b10-cmdctl, "

+ 5 - 15
src/bin/cmdctl/cmdctl.spec

@@ -7,42 +7,32 @@
         "item_name": "key_file",
         "item_type": "string",
         "item_optional": False,
-        "item_default": 'cmdctl-keyfile.pem'
+        "item_default": '@@SYSCONFDIR@@/@PACKAGE@/cmdctl-keyfile.pem'
       },
       {
         "item_name": "cert_file",
         "item_type": "string",
         "item_optional": False,
-        "item_default": 'cmdctl-certfile.pem'
+        "item_default": '@@SYSCONFDIR@@/@PACKAGE@/cmdctl-certfile.pem'
       },
       {
         "item_name": "accounts_file",
         "item_type": "string",
         "item_optional": False,
-        "item_default": 'cmdctl-accounts.csv'
+        "item_default": '@@SYSCONFDIR@@/@PACKAGE@/cmdctl-accounts.csv'
       }
     ],
     "commands": [
       {
-        "command_name": "print_message",
-        "command_description": "Print the given message to stdout",
-        "command_args": [ {
-          "item_name": "message",
-          "item_type": "string",
-          "item_optional": False,
-          "item_default": ""
-        } ]
-      },
-      {
         "command_name": "print_settings",
         "command_description": "Print some_string and some_int to stdout",
         "command_args": []
       },
       {
         "command_name": "shutdown",
-        "command_description": "Shut down cmdctl",
+        "command_description": "shutdown cmdctl",
         "command_args": []
-      }
+      },
     ]
   }
 }

+ 2 - 0
src/bin/cmdctl/tests/Makefile.am

@@ -8,5 +8,7 @@ check-local:
 	for pytest in $(PYTESTS) ; do \
 	echo Running test: $$pytest ; \
 	env PYTHONPATH=$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python:$(abs_top_builddir)/src/bin/cmdctl \
+	CMDCTL_SPEC_PATH=$(abs_top_builddir)/src/bin/cmdctl \
+	CMDCTL_SRC_PATH=$(abs_top_srcdir)/src/bin/cmdctl \
 	$(PYCOVERAGE) $(abs_srcdir)/$$pytest ; \
 	done

+ 2 - 2
src/bin/cmdctl/tests/cmdctl_test.in

@@ -18,10 +18,10 @@
 PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
 export PYTHON_EXEC
 
-BINDCTL_TEST_PATH=@abs_top_srcdir@/src/bin/cmdctl/tests
+CMDCTL_TEST_PATH=@abs_top_srcdir@/src/bin/cmdctl/tests
 PYTHONPATH=@abs_top_srcdir@/src/bin/cmdctl
 export PYTHONPATH
 
-cd ${BINDCTL_TEST_PATH}
+cd ${CMDCTL_TEST_PATH}
 exec ${PYTHON_EXEC} -O cmdctl_test.py $*
 

+ 185 - 20
src/bin/cmdctl/tests/cmdctl_test.py

@@ -16,8 +16,17 @@
 
 import unittest
 import socket
+import tempfile
 from cmdctl import *
 
+SPEC_FILE_PATH = '..' + os.sep
+if 'CMDCTL_SPEC_PATH' in os.environ:
+    SPEC_FILE_PATH = os.environ['CMDCTL_SPEC_PATH'] + os.sep
+
+SRC_FILE_PATH = '..' + os.sep
+if 'CMDCTL_SRC_PATH' in os.environ:
+    SRC_FILE_PATH = os.environ['CMDCTL_SRC_PATH'] + os.sep
+
 # Rewrite the class for unittest.
 class MySecureHTTPRequestHandler(SecureHTTPRequestHandler):
     def __init__(self):
@@ -42,17 +51,20 @@ class MySecureHTTPRequestHandler(SecureHTTPRequestHandler):
         os.remove('tmp.file')
     
 
-class MySecureHTTPServer(SecureHTTPServer):
+class FakeSecureHTTPServer(SecureHTTPServer):
     def __init__(self):
         self.user_sessions = {}
+        self.cmdctl = FakeCommandControlForTestRequestHandler()
+        self._verbose = True 
+        self._user_infos = {}
         self.idle_timeout = 1200
-        self.cmdctrl = MyCommandControl()
-        self._verbose = False
+        self._lock = threading.Lock()
 
-class MyCommandControl(CommandControl):
+class FakeCommandControlForTestRequestHandler(CommandControl):
     def __init__(self):
-        self.config_data = {}
-        self.module_spec = {}
+        self._config_data = {}
+        self.modules_spec = {}
+        self._lock = threading.Lock()
 
     def send_command(self, mod, cmd, param):
         return 0, {}
@@ -60,14 +72,17 @@ class MyCommandControl(CommandControl):
 
 class TestSecureHTTPRequestHandler(unittest.TestCase):
     def setUp(self):
+        self.old_stdout = sys.stdout
+        sys.stdout = open(os.devnull, 'w')
         self.handler = MySecureHTTPRequestHandler()
-        self.handler.server = MySecureHTTPServer()
+        self.handler.server = FakeSecureHTTPServer()
         self.handler.server.user_sessions = {}
-        self.handler.server.user_infos = {}
+        self.handler.server._user_infos = {}
         self.handler.headers = {}
         self.handler.rfile = open("check.tmp", 'w+b')
 
     def tearDown(self):
+        sys.stdout = self.old_stdout
         self.handler.rfile.close()
         os.remove('check.tmp')
 
@@ -145,7 +160,7 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
     def test_check_user_name_and_pwd(self):
         self.handler.headers = {}
         ret, msg = self.handler._check_user_name_and_pwd()
-        self.assertTrue(ret == False)
+        self.assertFalse(ret)
         self.assertEqual(msg, ['invalid username or password'])
 
     def test_check_user_name_and_pwd_1(self):
@@ -154,9 +169,9 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
         self.handler.headers['Content-Length'] = len
         self.handler.rfile.seek(0, 0)
 
-        self.handler.server.user_infos['root'] = ['aa', 'aaa']
+        self.handler.server._user_infos['root'] = ['aa', 'aaa']
         ret, msg = self.handler._check_user_name_and_pwd()
-        self.assertTrue(ret == False)
+        self.assertFalse(ret)
         self.assertEqual(msg, ['password doesn\'t match'])
 
     def test_check_user_name_and_pwd_2(self):
@@ -166,7 +181,7 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
         self.handler.rfile.seek(0, 0)
 
         ret, msg = self.handler._check_user_name_and_pwd()
-        self.assertTrue(ret == False)
+        self.assertFalse(ret)
         self.assertEqual(msg, ['invalid username or password'])
 
     def test_check_user_name_and_pwd_3(self):
@@ -176,7 +191,7 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
         self.handler.rfile.seek(0, 0)
 
         ret, msg = self.handler._check_user_name_and_pwd()
-        self.assertTrue(ret == False)
+        self.assertFalse(ret)
         self.assertEqual(msg, ['need user name'])
 
     def test_check_user_name_and_pwd_4(self):
@@ -185,9 +200,9 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
         self.handler.headers['Content-Length'] = len
         self.handler.rfile.seek(0, 0)
 
-        self.handler.server.user_infos['root'] = ['aa', 'aaa']
+        self.handler.server._user_infos['root'] = ['aa', 'aaa']
         ret, msg = self.handler._check_user_name_and_pwd()
-        self.assertTrue(ret == False)
+        self.assertFalse(ret)
         self.assertEqual(msg, ['need password'])
 
     def test_check_user_name_and_pwd_5(self):
@@ -197,7 +212,7 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
         self.handler.rfile.seek(0, 0)
 
         ret, msg = self.handler._check_user_name_and_pwd()
-        self.assertTrue(ret == False)
+        self.assertFalse(ret)
         self.assertEqual(msg, ['user doesn\'t exist'])
 
     def test_do_POST(self):
@@ -247,8 +262,8 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
 
         self.handler.rfile.seek(0, 0)
         self.handler.path = '/module/command'
-        self.handler.server.cmdctrl.module_spec = {}
-        self.handler.server.cmdctrl.module_spec['module'] = self._gen_module_spec()
+        self.handler.server.cmdctl.modules_spec = {}
+        self.handler.server.cmdctl.modules_spec['module'] = self._gen_module_spec()
         rcode, reply = self.handler._handle_post_request()
         self.assertEqual(http.client.OK, rcode)
 
@@ -259,10 +274,160 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
 
         self.handler.rfile.seek(0, 0)
         self.handler.path = '/module/command'
-        self.handler.server.cmdctrl.module_spec = {}
-        self.handler.server.cmdctrl.module_spec['module'] = self._gen_module_spec()
+        self.handler.server.cmdctl.modules_spec = {}
+        self.handler.server.cmdctl.modules_spec['module'] = self._gen_module_spec()
         rcode, reply = self.handler._handle_post_request()
         self.assertEqual(http.client.BAD_REQUEST, rcode)
 
+class MyCommandControl(CommandControl):
+    def _get_modules_specification(self):
+        return {}
+
+    def _get_config_data_from_config_manager(self):
+        return {}
+
+    def _setup_session(self):
+        spec_file = SPEC_FILE_PATH + 'cmdctl.spec'
+        module_spec = isc.config.module_spec_from_file(spec_file)
+        config = isc.config.config_data.ConfigData(module_spec)
+        self._module_name = 'Cmdctl'
+        self._cmdctl_config_data = config.get_full_config()
+
+    def _handle_msg_from_msgq(self): 
+        pass
+
+class TestCommandControl(unittest.TestCase):
+
+    def setUp(self):
+        self.old_stdout = sys.stdout
+        sys.stdout = open(os.devnull, 'w')
+        self.cmdctl = MyCommandControl(None, True)
+   
+    def tearDown(self):
+        sys.stdout = self.old_stdout
+
+    def _check_config(self, cmdctl):
+        key, cert, account = cmdctl.get_cmdctl_config_data()
+        self.assertIsNotNone(key)
+        self.assertIsNotNone(cert)
+        self.assertIsNotNone(account)
+
+    def test_get_cmdctl_config_data(self):
+        old_env = os.environ
+        if 'B10_FROM_SOURCE' in os.environ:
+            del os.environ['B10_FROM_SOURCE']
+        self.cmdctl.get_cmdctl_config_data() 
+        self._check_config(self.cmdctl)
+        os.environ = old_env
+
+        old_env = os.environ
+        os.environ['B10_FROM_SOURCE'] = '../'
+        self._check_config(self.cmdctl)
+        os.environ = old_env
+    
+    def test_parse_command_result(self):
+        self.assertEqual({}, self.cmdctl._parse_command_result(1, {'error' : 1}))
+        self.assertEqual({'a': 1}, self.cmdctl._parse_command_result(0, {'a' : 1}))
+
+    def _check_answer(self, answer, rcode_, msg_):
+        rcode, msg = ccsession.parse_answer(answer)
+        self.assertEqual(rcode, rcode_)
+        self.assertEqual(msg, msg_)
+
+    def test_command_handler(self):
+        answer = self.cmdctl.command_handler('unknown-command', None)
+        self._check_answer(answer, 1, 'unknown command: unknown-command')
+
+        answer = self.cmdctl.command_handler('print_settings', None)
+        rcode, msg = ccsession.parse_answer(answer)
+        self.assertEqual(rcode, 0)
+        self.assertTrue(msg != None)
+
+    def test_check_config_handler(self):
+        answer = self.cmdctl.config_handler({'non-exist': 123})
+        self._check_answer(answer, 1, 'unknown config item: non-exist')
+
+        old_env = os.environ
+        os.environ['B10_FROM_SOURCE'] = '../'
+        self._check_config(self.cmdctl)
+        os.environ = old_env
+
+        answer = self.cmdctl.config_handler({'key_file': '/user/non-exist_folder'})
+        self._check_answer(answer, 1, "the file doesn't exist: /user/non-exist_folder")
+
+        answer = self.cmdctl.config_handler({'cert_file': '/user/non-exist_folder'})
+        self._check_answer(answer, 1, "the file doesn't exist: /user/non-exist_folder")
+
+        answer = self.cmdctl.config_handler({'accounts_file': '/user/non-exist_folder'})
+        self._check_answer(answer, 1, 
+                "Invalid accounts file: [Errno 2] No such file or directory: '/user/non-exist_folder'")
+
+        # Test with invalid accounts file
+        file_name = 'tmp.account.file'
+        temp_file = open(file_name, 'w')
+        writer = csv.writer(temp_file)
+        writer.writerow(['a', 'b'])
+        temp_file.close()
+        answer = self.cmdctl.config_handler({'accounts_file': file_name})
+        self._check_answer(answer, 1, "Invalid accounts file: list index out of range")
+        os.remove(file_name)
+    
+    def test_send_command(self):
+        rcode, value = self.cmdctl.send_command('Cmdctl', 'print_settings', None)
+        self.assertEqual(rcode, 0)
+
+class MySecureHTTPServer(SecureHTTPServer):
+    def server_bind(self):
+        pass
+
+class TestSecureHTTPServer(unittest.TestCase):
+    def setUp(self):
+        self.old_stdout = sys.stdout
+        sys.stdout = open(os.devnull, 'w')
+        self.server = MySecureHTTPServer(('localhost', 8080), 
+                                         MySecureHTTPRequestHandler,
+                                         MyCommandControl, verbose=True)
+
+    def tearDown(self):
+        sys.stdout = self.old_stdout
+
+    def test_create_user_info(self):
+        self.server._create_user_info('/local/not-exist')
+        self.assertEqual(0, len(self.server._user_infos))
+
+        self.server._create_user_info(SRC_FILE_PATH + 'cmdctl-accounts.csv')
+        self.assertEqual(1, len(self.server._user_infos))
+        self.assertTrue('root' in self.server._user_infos)
+
+    def test_check_key_and_cert(self):
+        self.assertRaises(CmdctlException, self.server._check_key_and_cert,
+                         '/local/not-exist', 'cmdctl-keyfile.pem')
+
+        self.server._check_key_and_cert(SRC_FILE_PATH + 'cmdctl-keyfile.pem',
+                                        SRC_FILE_PATH + 'cmdctl-certfile.pem')
+
+    def test_wrap_sock_in_ssl_context(self):
+        sock = socket.socket()
+        self.assertRaises(socket.error, 
+                          self.server._wrap_socket_in_ssl_context,
+                          sock, 
+                          '../cmdctl-keyfile',
+                          '../cmdctl-certfile')
+
+        sock1 = socket.socket()
+        self.server._wrap_socket_in_ssl_context(sock1, 
+                          SRC_FILE_PATH + 'cmdctl-keyfile.pem',
+                          SRC_FILE_PATH + 'cmdctl-certfile.pem')
+
+class TestFuncNotInClass(unittest.TestCase):
+    def test_check_port(self):
+        self.assertRaises(OptionValueError, check_port, None, 'port', -1, None)        
+        self.assertRaises(OptionValueError, check_port, None, 'port', 65536, None)        
+        self.assertRaises(OptionValueError, check_addr, None, 'ipstr', 'a.b.d', None)        
+        self.assertRaises(OptionValueError, check_addr, None, 'ipstr', '1::0:a.b', None)        
+
+
 if __name__== "__main__":
     unittest.main()
+
+

+ 1 - 1
src/bin/host/Makefile.am

@@ -7,7 +7,7 @@ CLEANFILES = *.gcno *.gcda
 
 bin_PROGRAMS = host
 host_SOURCES = host.cc
-host_LDADD = $(top_builddir)/src/lib/dns/.libs/libdns.a
+host_LDADD = $(top_builddir)/src/lib/dns/.libs/libdns++.a
 host_LDADD += $(top_builddir)/src/lib/exceptions/.libs/libexceptions.a
 
 #man_MANS = host.1

+ 2 - 0
src/bin/host/host.cc

@@ -19,6 +19,8 @@
 #include <sys/time.h>       // for gettimeofday
 #include <sys/socket.h>     // networking functions and definitions on FreeBSD
 
+#include <unistd.h>
+
 #include <string>
 #include <iostream>
 

+ 26 - 20
src/bin/loadzone/Makefile.am

@@ -1,3 +1,5 @@
+SUBDIRS = tests/correct
+SUBDIRS += tests/error
 bin_SCRIPTS = b10-loadzone
 
 CLEANFILES = b10-loadzone
@@ -22,23 +24,27 @@ install-data-local:
 	$(mkinstalldirs) $(DESTDIR)/@localstatedir@/@PACKAGE@
 # TODO: permissions handled later
 
-EXTRA_DIST += testdata/README
-EXTRA_DIST += testdata/dsset-subzone.example.com.
-EXTRA_DIST += testdata/example.com
-EXTRA_DIST += testdata/example.com.signed
-EXTRA_DIST += testdata/Kexample.com.+005+04456.key
-EXTRA_DIST += testdata/Kexample.com.+005+04456.private
-EXTRA_DIST += testdata/Kexample.com.+005+33495.key
-EXTRA_DIST += testdata/Kexample.com.+005+33495.private
-EXTRA_DIST += testdata/Ksql1.example.com.+005+12447.key
-EXTRA_DIST += testdata/Ksql1.example.com.+005+12447.private
-EXTRA_DIST += testdata/Ksql1.example.com.+005+33313.key
-EXTRA_DIST += testdata/Ksql1.example.com.+005+33313.private
-EXTRA_DIST += testdata/Ksql2.example.com.+005+38482.key
-EXTRA_DIST += testdata/Ksql2.example.com.+005+38482.private
-EXTRA_DIST += testdata/Ksql2.example.com.+005+63192.key
-EXTRA_DIST += testdata/Ksql2.example.com.+005+63192.private
-EXTRA_DIST += testdata/sql1.example.com
-EXTRA_DIST += testdata/sql1.example.com.signed
-EXTRA_DIST += testdata/sql2.example.com
-EXTRA_DIST += testdata/sql2.example.com.signed
+EXTRA_DIST += tests/normal/README
+EXTRA_DIST += tests/normal/dsset-subzone.example.com
+EXTRA_DIST += tests/normal/example.com
+EXTRA_DIST += tests/normal/example.com.signed
+EXTRA_DIST += tests/normal/Kexample.com.+005+04456.key
+EXTRA_DIST += tests/normal/Kexample.com.+005+04456.private
+EXTRA_DIST += tests/normal/Kexample.com.+005+33495.key
+EXTRA_DIST += tests/normal/Kexample.com.+005+33495.private
+EXTRA_DIST += tests/normal/Ksql1.example.com.+005+12447.key
+EXTRA_DIST += tests/normal/Ksql1.example.com.+005+12447.private
+EXTRA_DIST += tests/normal/Ksql1.example.com.+005+33313.key
+EXTRA_DIST += tests/normal/Ksql1.example.com.+005+33313.private
+EXTRA_DIST += tests/normal/Ksql2.example.com.+005+38482.key
+EXTRA_DIST += tests/normal/Ksql2.example.com.+005+38482.private
+EXTRA_DIST += tests/normal/Ksql2.example.com.+005+63192.key
+EXTRA_DIST += tests/normal/Ksql2.example.com.+005+63192.private
+EXTRA_DIST += tests/normal/sql1.example.com
+EXTRA_DIST += tests/normal/sql1.example.com.signed
+EXTRA_DIST += tests/normal/sql2.example.com
+EXTRA_DIST += tests/normal/sql2.example.com.signed
+
+pytest:
+	$(SHELL) tests/correct/correct_test.sh
+	$(SHELL) tests/error/error_test.sh

+ 16 - 6
src/bin/loadzone/b10-loadzone.py.in

@@ -19,7 +19,8 @@ import sys; sys.path.append ('@@PYTHONPATH@@')
 import re, getopt
 import isc.datasrc
 from isc.datasrc.master import MasterFile
-
+import time
+import os
 #########################################################################
 # usage: print usage note and exit
 #########################################################################
@@ -57,23 +58,32 @@ def main():
     if len(args) != 1:
         usage()
     zonefile = args[0]
-
+    verbose = os.isatty(sys.stdout.fileno())
     try:
-        master = MasterFile(zonefile, initial_origin)
+        master = MasterFile(zonefile, initial_origin, verbose)
     except Exception as e:
-        print("Error reading zone file: " + str(e))
+        sys.stderr.write("Error reading zone file: %s\n" % str(e))
         exit(1)
 
     try:
         zone = master.zonename()
+        if verbose:
+            sys.stdout.write("Using SQLite3 database file %s\n" % dbfile)
+            sys.stdout.write("Zone name is %s\n" % zone)
+            sys.stdout.write("Loading file \"%s\"\n" % zonefile)
     except Exception as e:
-        print("Error reading zone file: " + str(e))
+        sys.stdout.write("\n")
+        sys.stderr.write("Error reading zone file: %s\n" % str(e))
         exit(1)
 
     try:
         isc.datasrc.sqlite3_ds.load(dbfile, zone, master.zonedata)
+        if verbose:
+            master.closeverbose()
+            sys.stdout.write("\nDone.\n")
     except Exception as e:
-        print("Error loading database: " + str(e))
+        sys.stdout.write("\n")
+        sys.stderr.write("Error loading database: %s\n"% str(e))
         exit(1)
 
 if __name__ == "__main__":

+ 1 - 1
src/bin/loadzone/run_loadzone.sh.in

@@ -21,5 +21,5 @@ export PYTHON_EXEC
 PYTHONPATH=@abs_top_builddir@/src/lib/python
 export PYTHONPATH
 
-LOADZONE_PATH=@abs_top_srcdir@/src/bin/loadzone
+LOADZONE_PATH=@abs_top_builddir@/src/bin/loadzone
 exec ${LOADZONE_PATH}/b10-loadzone $*

+ 25 - 0
src/bin/loadzone/tests/correct/Makefile.am

@@ -0,0 +1,25 @@
+PYTESTS = correct_test.sh
+EXTRA_DIST = get_zonedatas.py
+EXTRA_DIST += include.db
+EXTRA_DIST += inclsub.db
+EXTRA_DIST += known.test.out
+EXTRA_DIST += mix1.db
+EXTRA_DIST += mix1sub1.db
+EXTRA_DIST += mix1sub2.db
+EXTRA_DIST += mix2.db
+EXTRA_DIST += mix2sub1.txt
+EXTRA_DIST += mix2sub2.txt
+EXTRA_DIST += ttl1.db
+EXTRA_DIST += ttl2.db
+EXTRA_DIST += ttlext.db
+EXTRA_DIST += example.db
+
+# later will have configure option to choose this, like: coverage run --branch
+PYCOVERAGE = $(PYTHON)
+# test using command-line arguments, so use check-local target instead of TESTS
+check-local:
+	for pytest in $(PYTESTS) ; do \
+	echo Running test: $$pytest ; \
+	env PYTHONPATH=$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python:$(abs_top_builddir)/src/bin/loadzone \
+	$(SHELL) $(abs_builddir)/$$pytest ; \
+	done

+ 75 - 0
src/bin/loadzone/tests/correct/correct_test.sh.in

@@ -0,0 +1,75 @@
+#! /bin/sh
+
+# Copyright (C) 2010  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
+export PYTHON_EXEC
+
+PYTHONPATH=@abs_top_srcdir@/src/lib/python:@abs_top_builddir@/src/lib/python
+export PYTHONPATH
+
+LOADZONE_PATH=@abs_top_builddir@/src/bin/loadzone
+TEST_FILE_PATH=@abs_top_srcdir@/src/bin/loadzone/tests/correct
+TEST_OUTPUT_PATH=@abs_top_builddir@/src/bin/loadzone//tests/correct
+
+status=0
+echo "Loadzone include. from include.db file"
+cd ${TEST_FILE_PATH}
+${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 include.db >> /dev/null
+
+echo "loadzone  ttl1. from ttl1.db file"
+${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 ttl1.db >> /dev/null
+
+echo "loadzone ttl2. from ttl2.db file"
+${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 ttl2.db >> /dev/null
+
+echo "loadzone mix1. from mix1.db"
+${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 mix1.db >> /dev/null
+
+echo "loadzone mix2. from mix2.db"
+${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 mix2.db >> /dev/null
+
+echo "loadzone ttlext. from ttlext.db"
+${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 ttlext.db >> /dev/null
+
+echo "loadzone example.com. from example.db"
+${LOADZONE_PATH}/b10-loadzone -d ${TEST_OUTPUT_PATH}/zone.sqlite3 example.db >> /dev/null
+
+echo "I:test master file \$INCLUDE semantics"
+echo "I:test master file BIND 8 compatibility TTL and \$TTL semantics"
+echo "I:test master file RFC1035 TTL and \$TTL semantics"
+echo "I:test master file BIND8 compatibility and mixed \$INCLUDE with \$TTL semantics"
+echo "I:test master file RFC1035 TTL and mixed \$INCLUDE with \$TTL semantics"
+echo "I:test master file BIND9 extenstion of TTL"
+echo "I:test master file RFC1035 missing CLASS, TTL, NAME semantics"
+
+${PYTHON_EXEC} ${TEST_FILE_PATH}/get_zonedatas.py ${TEST_OUTPUT_PATH}/zone.sqlite3 > ${TEST_OUTPUT_PATH}/test.out
+echo "Compare test results."
+diff ${TEST_OUTPUT_PATH}/test.out ${TEST_FILE_PATH}/known.test.out || status=1
+
+echo "Clean tmp files."
+rm -f ${TEST_OUTPUT_PATH}/zone.sqlite3
+rm -f ${TEST_OUTPUT_PATH}/test.out
+echo "I:exit status: $status"
+echo "------------------------------------------------------------------------------"
+echo "Ran 7 test files"
+echo ""
+if [ "$status" -eq 1 ] ;then
+    echo "ERROR"
+else
+    echo "OK"
+fi
+exit $status

+ 14 - 0
src/bin/loadzone/tests/correct/example.db

@@ -0,0 +1,14 @@
+;This file includes all kinds of rr form. missing name, ttl and class or not.
+$ORIGIN example.com.
+$TTL 60
+@    IN SOA   ns1.example.com. hostmaster.example.com. (1 43200 900 1814400 7200)
+     IN     20      NS  ns1
+                    NS  ns2
+ns1  IN     30      A   192.168.1.102
+            70      NS  ns3
+     IN             NS  ns4
+     10     IN      MX  10  mail.example.com.
+ns2         80      A   1.1.1.1
+ns3  IN             A   2.2.2.2
+ns4                 A   3.3.3.3
+ns5  90     IN      A   4.4.4.4

+ 8 - 0
src/bin/loadzone/tests/correct/get_zonedatas.py

@@ -0,0 +1,8 @@
+from isc.datasrc import sqlite3_ds
+import sys
+ZONE_FILE = sys.argv[1]
+zonename_set = ["include.", "ttl1.", "ttl2.", "mix1.", "mix2.", "ttlext.", "example.com."]
+for zone_name in zonename_set:
+    for rr_data in sqlite3_ds.get_zone_datas(zone_name, ZONE_FILE):
+        data_len = len(rr_data[2])
+        sys.stdout.write(rr_data[2] + '\t\t' + str(rr_data[4]) + '\tIN\t' + rr_data[5] + '\t' + rr_data[7] + '\n')

+ 4 - 0
src/bin/loadzone/tests/correct/inclsub.db

@@ -0,0 +1,4 @@
+a    300		A	10.0.1.1
+$ORIGIN foo
+b	 300     	A	10.0.2.2
+

+ 23 - 0
src/bin/loadzone/tests/correct/include.db

@@ -0,0 +1,23 @@
+$ORIGIN include.   ; initialize origin
+$TTL 300
+@			IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3600
+				)
+			NS	ns
+
+ns			A	127.0.0.1
+
+a			A	10.0.0.1
+$INCLUDE inclsub.db sub   ;  a.include. is the relative domain name origin for the included file
+; use the current domain name
+            A	99.99.99.99
+b			A	10.0.0.2
+$ORIGIN b
+$INCLUDE inclsub.db
+; use the current domain name
+			A	10.0.0.99
+c			A	10.0.0.3

+ 79 - 0
src/bin/loadzone/tests/correct/known.test.out

@@ -0,0 +1,79 @@
+include.		300	IN	SOA	ns.include. hostmaster.include. 1 3600 1800 1814400 3600
+include.		300	IN	NS	ns.include.
+ns.include.		300	IN	A	127.0.0.1
+a.include.		300	IN	A	10.0.0.1
+a.sub.include.		300	IN	A	10.0.1.1
+b.foo.sub.include.		300	IN	A	10.0.2.2
+a.include.		300	IN	A	99.99.99.99
+b.include.		300	IN	A	10.0.0.2
+a.b.include.		300	IN	A	10.0.1.1
+b.foo.b.include.		300	IN	A	10.0.2.2
+b.include.		300	IN	A	10.0.0.99
+c.b.include.		300	IN	A	10.0.0.3
+ttl1.		3	IN	SOA	ns.ttl1. hostmaster.ttl1. 1 3600 1800 1814400 3
+ttl1.		3	IN	NS	ns.ttl1.
+ns.ttl1.		3	IN	A	10.53.0.1
+a.ttl1.		3	IN	TXT	"soa minttl 3"
+b.ttl1.		2	IN	TXT	"explicit ttl 2"
+c.ttl1.		3	IN	TXT	"soa minttl 3"
+d.ttl1.		1	IN	TXT	"default ttl 1"
+e.ttl1.		4	IN	TXT	"explicit ttl 4"
+f.ttl1.		1	IN	TXT	"default ttl 1"
+ttl2.		1	IN	SOA	ns.ttl2. hostmaster.ttl2. 1 3600 1800 1814400 3
+ttl2.		1	IN	NS	ns.ttl2.
+ns.ttl2.		1	IN	A	10.53.0.1
+a.ttl2.		1	IN	TXT	"inherited ttl 1"
+b.ttl2.		2	IN	TXT	"explicit ttl 2"
+c.ttl2.		2	IN	TXT	"inherited ttl 2"
+d.ttl2.		3	IN	TXT	"default ttl 3"
+e.ttl2.		2	IN	TXT	"explicit ttl 2"
+f.ttl2.		3	IN	TXT	"default ttl 3"
+mix1.		3	IN	SOA	ns.mix1. hostmaster.mix1. 1 3600 1800 1814400 3
+mix1.		3	IN	NS	ns.mix1.
+ns.mix1.		3	IN	A	10.53.0.1
+a.mix1.		3	IN	TXT	"soa minttl 3"
+b.mix1.		2	IN	TXT	"explicit ttl 2"
+a.mix1.		3	IN	TXT	"soa minttl 3"
+b.foo.mix1.		3	IN	TXT	"soa minttl 3"
+c.mix1.		3	IN	TXT	"soa minttl 3"
+d.mix1.		1	IN	TXT	"default ttl 1"
+e.mix1.		4	IN	TXT	"explicit ttl 4"
+f.mix1.		1	IN	TXT	"default ttl 1"
+i.mix1.		1	IN	TXT	"default ttl 1"
+g.mix1.		5	IN	TXT	"default ttl 5"
+h.mix1.		5	IN	TXT	"the include ttl 5"
+mix2.		1	IN	SOA	ns.mix2. hostmaster.mix2. 1 3600 1800 1814400 3
+mix2.		1	IN	NS	ns.mix2.
+ns.mix2.		1	IN	A	10.53.0.1
+a.mix2.		1	IN	TXT	"inherited ttl 1"
+h.mix2.		1	IN	TXT	"inherited ttl 1"
+g.mix2.		6	IN	TXT	"inherited ttl 6"
+b.mix2.		6	IN	TXT	"explicit ttl 6"
+c.mix2.		2	IN	TXT	"inherited ttl 2"
+m.mix2.		6	IN	TXT	"explicit ttl 6"
+d.mix2.		3	IN	TXT	"default ttl 3"
+e.mix2.		2	IN	TXT	"explicit ttl 2"
+n.mix2.		3	IN	TXT	"default ttl 3"
+f.mix2.		3	IN	TXT	"default ttl 3"
+g.mix2.		5	IN	TXT	"default ttl 5"
+f.mix2.		5	IN	TXT	"default ttl 5"
+ttlext.		3	IN	SOA	ns.ttlext. hostmaster.ttlext. 1 3600 1800 1814400 3
+ttlext.		3	IN	NS	ns.ttlext.
+ns.ttlext.		3	IN	A	10.53.0.1
+a.ttlext.		3	IN	TXT	"soa minttl 3"
+b.ttlext.		2	IN	TXT	"explicit ttl 2"
+c.ttlext.		3	IN	TXT	"soa minttl 3"
+d.ttlext.		600	IN	TXT	"default ttl 600"
+e.ttlext.		4	IN	TXT	"explicit ttl 4"
+f.ttlext.		600	IN	TXT	"default ttl 600"
+example.com.		60	IN	SOA	ns1.example.com. hostmaster.example.com. 1 43200 900 1814400 7200
+example.com.		20	IN	NS	ns1.example.com.
+example.com.		60	IN	NS	ns2.example.com.
+ns1.example.com.		30	IN	A	192.168.1.102
+ns1.example.com.		70	IN	NS	ns3.example.com.
+ns1.example.com.		60	IN	NS	ns4.example.com.
+ns1.example.com.		10	IN	MX	10 mail.example.com.
+ns2.example.com.		80	IN	A	1.1.1.1
+ns3.example.com.		60	IN	A	2.2.2.2
+ns4.example.com.		60	IN	A	3.3.3.3
+ns5.example.com.		90	IN	A	4.4.4.4

+ 21 - 0
src/bin/loadzone/tests/correct/mix1.db

@@ -0,0 +1,21 @@
+; $Id: ttl1.db,v 1.6 2007/06/19 23:47:04 tbox Exp $
+$ORIGIN mix1.
+@			IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3
+				)
+			NS	ns
+ns			A	10.53.0.1
+a			TXT	"soa minttl 3"
+b		2	TXT	"explicit ttl 2"
+$INCLUDE mix1sub1.db
+c			TXT	"soa minttl 3"
+$TTL 1
+d			TXT	"default ttl 1"
+e		4	TXT	"explicit ttl 4"
+f			TXT	"default ttl 1"
+$INCLUDE mix1sub2.db
+h       5   TXT "the include ttl 5"

+ 3 - 0
src/bin/loadzone/tests/correct/mix1sub1.db

@@ -0,0 +1,3 @@
+a                       TXT      "soa minttl 3"
+$ORIGIN foo
+b                       TXT      "soa minttl 3"

+ 3 - 0
src/bin/loadzone/tests/correct/mix1sub2.db

@@ -0,0 +1,3 @@
+i                       TXT      "default ttl 1"
+$TTL   5
+g                       TXT      "default ttl 5"

+ 21 - 0
src/bin/loadzone/tests/correct/mix2.db

@@ -0,0 +1,21 @@
+$ORIGIN mix2.
+@		1	IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3
+				)
+			NS	ns
+ns			A	10.53.0.1
+a			TXT	"inherited ttl 1"
+$INCLUDE mix2sub1.txt
+b			TXT	"explicit ttl 6"
+c		2	TXT	"inherited ttl 2"
+m           TXT "explicit ttl 6"
+$TTL 3
+d			TXT	"default ttl 3"
+e		2	TXT	"explicit ttl 2"
+n           TXT "default ttl 3"
+$INCLUDE mix2sub2.txt
+f			TXT	"default ttl 5"

+ 3 - 0
src/bin/loadzone/tests/correct/mix2sub1.txt

@@ -0,0 +1,3 @@
+h                       TXT     "inherited ttl 1"
+$TTL 6
+g                       TXT     "inherited ttl 6"

+ 3 - 0
src/bin/loadzone/tests/correct/mix2sub2.txt

@@ -0,0 +1,3 @@
+f                       TXT     "default  ttl 3"
+$TTL 5
+g                       TXT     "default  ttl 5"

+ 17 - 0
src/bin/loadzone/tests/correct/ttl1.db

@@ -0,0 +1,17 @@
+$ORIGIN ttl1.
+@			IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3
+				)
+			NS	ns
+ns			A	10.53.0.1
+a			TXT	"soa minttl 3"
+b		2	TXT	"explicit ttl 2"
+c			TXT	"soa minttl 3"
+$TTL 1
+d			TXT	"default ttl 1"
+e		4	TXT	"explicit ttl 4"
+f			TXT	"default ttl 1"

+ 17 - 0
src/bin/loadzone/tests/correct/ttl2.db

@@ -0,0 +1,17 @@
+$ORIGIN ttl2.
+@		1	IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3
+				)
+			NS	ns
+ns			A	10.53.0.1
+a			TXT	"inherited ttl 1"
+b		2	TXT	"explicit ttl 2"
+c			TXT	"inherited ttl 2"
+$TTL 3    ; a new ttl
+d			TXT	"default ttl 3"
+e		2	TXT	"explicit ttl 2"
+f			TXT	"default ttl 3"

+ 18 - 0
src/bin/loadzone/tests/correct/ttlext.db

@@ -0,0 +1,18 @@
+; $Id: ttl1.db,v 1.6 2007/06/19 23:47:04 tbox Exp $
+$ORIGIN ttlext.
+@			IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3
+				)
+			NS	ns
+ns			A	10.53.0.1
+a			TXT	"soa minttl 3"
+b		2S	TXT	"explicit ttl 2"
+c			TXT	"soa minttl 3"
+$TTL 10M  ; bind9 extention ttl
+d			TXT	"default ttl 600"
+e		4	TXT	"explicit ttl 4"
+f			TXT	"default ttl 600"

+ 25 - 0
src/bin/loadzone/tests/error/Makefile.am

@@ -0,0 +1,25 @@
+PYTESTS = error_test.sh
+
+EXTRA_DIST = error.known
+EXTRA_DIST += formerr1.db 
+EXTRA_DIST += formerr2.db
+EXTRA_DIST += formerr3.db
+EXTRA_DIST += formerr4.db
+EXTRA_DIST += formerr5.db
+EXTRA_DIST += include.txt
+EXTRA_DIST += keyerror1.db
+EXTRA_DIST += keyerror2.db
+EXTRA_DIST += keyerror3.db
+#EXTRA_DIST += nofilenane.db
+EXTRA_DIST += originerr1.db
+EXTRA_DIST += originerr2.db
+
+# later will have configure option to choose this, like: coverage run --branch
+PYCOVERAGE = $(PYTHON)
+# test using command-line arguments, so use check-local target instead of TESTS
+check-local:
+	for pytest in $(PYTESTS) ; do \
+	echo Running test: $$pytest ; \
+	env PYTHONPATH=$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/bin/loadzone \
+	$(SHELL) $(abs_builddir)/$$pytest ; \
+	done

+ 11 - 0
src/bin/loadzone/tests/error/error.known

@@ -0,0 +1,11 @@
+Error reading zone file: Cannot parse RR, No $ORIGIN: @ IN SOA ns hostmaster 1 3600 1800 1814400 3600
+Error reading zone file: $ORIGIN is not absolute in record:$ORIGIN com
+Error reading zone file: Cannot parse RR: $TL 300
+Error reading zone file: Cannot parse RR: $OIGIN com.
+Error loading database: Error while loading com.: Cannot parse RR: $INLUDE file.txt
+Error loading database: Error while loading com.: Invalid $include format
+Error loading database: Error while loading com.: Cannot parse RR, No $ORIGIN:  include.txt sub
+Error reading zone file: Invalid TTL: ""
+Error reading zone file: Invalid TTL: "M"
+Error loading database: Error while loading com.: Cannot parse RR: b "no type error!"
+Error reading zone file: Could not open bogusfile

+ 82 - 0
src/bin/loadzone/tests/error/error_test.sh.in

@@ -0,0 +1,82 @@
+#! /bin/sh
+
+# Copyright (C) 2010  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
+export PYTHON_EXEC
+
+PYTHONPATH=@abs_top_srcdir@/src/lib/python:@abs_top_builddir@/src/lib/python
+export PYTHONPATH
+
+LOADZONE_PATH=@abs_top_builddir@/src/bin/loadzone
+TEST_OUTPUT_PATH=@abs_top_builddir@/src/bin/loadzone/tests/error
+TEST_FILE_PATH=@abs_top_srcdir@/src/bin/loadzone/tests/error
+
+cd ${LOADZONE_PATH}/tests/error
+
+export LOADZONE_PATH
+status=0
+
+echo "PYTHON PATH: $PYTHONPATH"
+
+echo "Test no \$ORIGIN error in zone file"
+${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/originerr1.db 1> /dev/null 2> error.out
+${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/originerr2.db 1> /dev/null 2>> error.out
+
+echo "Test: key word TTL spell error"
+${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/keyerror1.db 1> /dev/null 2>> error.out
+
+echo "Test: key word ORIGIN spell error"
+${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/keyerror2.db 1> /dev/null 2>> error.out
+
+echo "Test: key INCLUDE spell error"
+${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/keyerror3.db 1> /dev/null 2>> error.out
+
+echo "Test: include formal error, miss filename"
+${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/formerr1.db 1> /dev/null 2>>error.out
+
+echo "Test: include form error, domain is not absolute"
+${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/formerr2.db 1> /dev/null 2>> error.out
+
+echo "Test: TTL form error, no ttl value"
+${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/formerr3.db 1> /dev/null 2>> error.out
+
+echo "Test: TTL form error, ttl value error"
+${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/formerr4.db 1> /dev/null 2>> error.out
+
+echo "Test: rr form error, no type"
+${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  ${TEST_FILE_PATH}/formerr5.db 1> /dev/null 2>> error.out
+
+echo "Test: zone file is bogus"
+# since bogusfile doesn't exist anyway, we *don't* specify the directory
+${LOADZONE_PATH}/b10-loadzone -d zone.sqlite3  bogusfile 1> /dev/null 2>> error.out
+
+diff error.out ${TEST_FILE_PATH}/error.known || status=1
+
+echo "Clean tmp file."
+rm -f error.out
+rm -f zone.sqlite3
+
+echo "I:exit status:$status"
+echo "-----------------------------------------------------------------------------"
+echo "Ran 11 test files"
+echo ""
+if [ "$status" -eq 1 ];then
+    echo "ERROR"
+else 
+    echo "OK"
+fi
+exit $status

+ 13 - 0
src/bin/loadzone/tests/error/formerr1.db

@@ -0,0 +1,13 @@
+$TTL 300
+$ORIGIN com.
+@			IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3600
+				)
+			NS	ns
+ns			A	127.0.0.1
+$INCLUDE
+a			A	10.0.0.1

+ 12 - 0
src/bin/loadzone/tests/error/formerr2.db

@@ -0,0 +1,12 @@
+$TTL 300
+com.			IN SOA	ns.com. hostmaster.com. (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3600
+				)
+			NS	ns.example.com.
+ns.com.			A	127.0.0.1
+$INCLUDE include.txt sub
+a.com.			A	10.0.0.1

+ 12 - 0
src/bin/loadzone/tests/error/formerr3.db

@@ -0,0 +1,12 @@
+$TTL 
+$ORIGIN com.
+@			IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3600
+				)
+			NS	ns
+ns			A	127.0.0.1
+a			A	10.0.0.1

+ 12 - 0
src/bin/loadzone/tests/error/formerr4.db

@@ -0,0 +1,12 @@
+$TTL M
+$ORIGIN com.
+@			IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3600
+				)
+			NS	ns
+ns			A	127.0.0.1
+a			A	10.0.0.1

+ 13 - 0
src/bin/loadzone/tests/error/formerr5.db

@@ -0,0 +1,13 @@
+$TTL 2M
+$ORIGIN com.
+@			IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3600
+				)
+			NS	ns
+ns			A	127.0.0.1 ; ip value
+b               "no type error!"
+a			A	10.0.0.1

+ 1 - 0
src/bin/loadzone/tests/error/include.txt

@@ -0,0 +1 @@
+a  300 A 127.0.0.1

+ 12 - 0
src/bin/loadzone/tests/error/keyerror1.db

@@ -0,0 +1,12 @@
+$TL 300
+@ORIGIN com.
+@			IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3600
+				)
+			NS	ns
+ns			A	127.0.0.1
+a			A	10.0.0.1

+ 12 - 0
src/bin/loadzone/tests/error/keyerror2.db

@@ -0,0 +1,12 @@
+$TTL 300
+$OIGIN com.
+@			IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3600
+				)
+			NS	ns
+ns			A	127.0.0.1
+a			A	10.0.0.1

+ 13 - 0
src/bin/loadzone/tests/error/keyerror3.db

@@ -0,0 +1,13 @@
+$TTL 300
+$ORIGIN com.
+@			IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3600
+				)
+			NS	ns
+ns			A	127.0.0.1
+$INLUDE file.txt
+a			A	10.0.0.1

+ 11 - 0
src/bin/loadzone/tests/error/originerr1.db

@@ -0,0 +1,11 @@
+$TTL 300
+@			IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3600
+				)
+			NS	ns
+ns			A	127.0.0.1
+a			A	10.0.0.1

+ 12 - 0
src/bin/loadzone/tests/error/originerr2.db

@@ -0,0 +1,12 @@
+$TTL 300
+$ORIGIN com
+@			IN SOA	ns hostmaster (
+				1        ; serial
+				3600
+				1800
+				1814400
+				3600
+				)
+			NS	ns
+ns			A	127.0.0.1
+a			A	10.0.0.1

src/bin/loadzone/testdata/Kexample.com.+005+04456.key → src/bin/loadzone/tests/normal/Kexample.com.+005+04456.key


src/bin/loadzone/testdata/Kexample.com.+005+04456.private → src/bin/loadzone/tests/normal/Kexample.com.+005+04456.private


src/bin/loadzone/testdata/Kexample.com.+005+33495.key → src/bin/loadzone/tests/normal/Kexample.com.+005+33495.key


src/bin/loadzone/testdata/Kexample.com.+005+33495.private → src/bin/loadzone/tests/normal/Kexample.com.+005+33495.private


src/bin/loadzone/testdata/Ksql1.example.com.+005+12447.key → src/bin/loadzone/tests/normal/Ksql1.example.com.+005+12447.key


src/bin/loadzone/testdata/Ksql1.example.com.+005+12447.private → src/bin/loadzone/tests/normal/Ksql1.example.com.+005+12447.private


src/bin/loadzone/testdata/Ksql1.example.com.+005+33313.key → src/bin/loadzone/tests/normal/Ksql1.example.com.+005+33313.key


src/bin/loadzone/testdata/Ksql1.example.com.+005+33313.private → src/bin/loadzone/tests/normal/Ksql1.example.com.+005+33313.private


src/bin/loadzone/testdata/Ksql2.example.com.+005+38482.key → src/bin/loadzone/tests/normal/Ksql2.example.com.+005+38482.key


src/bin/loadzone/testdata/Ksql2.example.com.+005+38482.private → src/bin/loadzone/tests/normal/Ksql2.example.com.+005+38482.private


src/bin/loadzone/testdata/Ksql2.example.com.+005+63192.key → src/bin/loadzone/tests/normal/Ksql2.example.com.+005+63192.key


src/bin/loadzone/testdata/Ksql2.example.com.+005+63192.private → src/bin/loadzone/tests/normal/Ksql2.example.com.+005+63192.private


src/bin/loadzone/testdata/README → src/bin/loadzone/tests/normal/README


src/bin/loadzone/testdata/dsset-subzone.example.com. → src/bin/loadzone/tests/normal/dsset-subzone.example.com


src/bin/loadzone/testdata/example.com → src/bin/loadzone/tests/normal/example.com


src/bin/loadzone/testdata/example.com.signed → src/bin/loadzone/tests/normal/example.com.signed


src/bin/loadzone/testdata/sql1.example.com → src/bin/loadzone/tests/normal/sql1.example.com


src/bin/loadzone/testdata/sql1.example.com.signed → src/bin/loadzone/tests/normal/sql1.example.com.signed


src/bin/loadzone/testdata/sql2.example.com → src/bin/loadzone/tests/normal/sql2.example.com


src/bin/loadzone/testdata/sql2.example.com.signed → src/bin/loadzone/tests/normal/sql2.example.com.signed


+ 1 - 5
src/bin/xfrin/tests/Makefile.am

@@ -1,16 +1,12 @@
 PYTESTS = xfrin_test.py
 EXTRA_DIST = $(PYTESTS)
 
-if HAVE_BOOST_PYTHON
-
 # later will have configure option to choose this, like: coverage run --branch
 PYCOVERAGE = $(PYTHON)
 # test using command-line arguments, so use check-local target instead of TESTS
 check-local:
 	for pytest in $(PYTESTS) ; do \
 	echo Running test: $$pytest ; \
-	env PYTHONPATH=$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/bin/xfrin:$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python \
+	env PYTHONPATH=$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/bin/xfrin:$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python \
 	$(PYCOVERAGE) $(abs_srcdir)/$$pytest ; \
 	done
-
-endif

+ 43 - 41
src/bin/xfrin/tests/xfrin_test.py

@@ -23,7 +23,7 @@ from xfrin import *
 # Commonly used (mostly constant) test parameters
 #
 TEST_ZONE_NAME = "example.com"
-TEST_RRCLASS = rr_class.IN()
+TEST_RRCLASS = RRClass.IN()
 TEST_DB_FILE = 'db_file'
 TEST_MASTER_IPV4_ADDRESS = '127.0.0.1'
 TEST_MASTER_IPV4_ADDRINFO = (socket.AF_INET, socket.SOCK_STREAM,
@@ -37,16 +37,16 @@ TEST_MASTER_IPV6_ADDRINFO = (socket.AF_INET6, socket.SOCK_STREAM,
 # If some other process uses this port test will fail.
 TEST_MASTER_PORT = '53535'
 
-soa_rdata = create_rdata(rr_type.SOA(), TEST_RRCLASS,
-                         'master.example.com. admin.example.com ' +
-                         '1234 3600 1800 2419200 7200')
-soa_rrset = rrset(name(TEST_ZONE_NAME), TEST_RRCLASS, rr_type.SOA(),
-                  rr_ttl(3600))
+soa_rdata = Rdata(RRType.SOA(), TEST_RRCLASS,
+                  'master.example.com. admin.example.com ' +
+                  '1234 3600 1800 2419200 7200')
+soa_rrset = RRset(Name(TEST_ZONE_NAME), TEST_RRCLASS, RRType.SOA(),
+                  RRTTL(3600))
 soa_rrset.add_rdata(soa_rdata)
-example_axfr_question = question(name(TEST_ZONE_NAME), TEST_RRCLASS,
-                                 rr_type.AXFR())
-example_soa_question = question(name(TEST_ZONE_NAME), TEST_RRCLASS,
-                                 rr_type.SOA())
+example_axfr_question = Question(Name(TEST_ZONE_NAME), TEST_RRCLASS,
+                                 RRType.AXFR())
+example_soa_question = Question(Name(TEST_ZONE_NAME), TEST_RRCLASS,
+                                 RRType.SOA())
 default_questions = [example_axfr_question]
 default_answers = [soa_rrset]
 
@@ -121,26 +121,25 @@ class MockXfrinConnection(XfrinConnection):
         return len(data)
 
     def create_response_data(self, response = True, bad_qid = False,
-                             rcode = rcode.NOERROR(),
+                             rcode = Rcode.NOERROR(),
                              questions = default_questions,
                              answers = default_answers):
-        resp = message(message_mode.RENDER)
+        resp = Message(Message.RENDER)
         qid = self.qid
         if bad_qid:
             qid += 1
         resp.set_qid(qid)
-        resp.set_opcode(op_code.QUERY())
+        resp.set_opcode(Opcode.QUERY())
         resp.set_rcode(rcode)
         if response:
-            resp.set_header_flag(message_flag.QR())
+            resp.set_header_flag(MessageFlag.QR())
         [resp.add_question(q) for q in questions]
-        [resp.add_rrset(section.ANSWER(), a) for a in answers]
+        [resp.add_rrset(Section.ANSWER(), a) for a in answers]
 
-        obuf = output_buffer(0)
-        renderer = message_render(obuf)
+        renderer = MessageRenderer()
         resp.to_wire(renderer)
-        reply_data = struct.pack('H', socket.htons(obuf.get_length()))
-        reply_data += obuf.get_data()
+        reply_data = struct.pack('H', socket.htons(renderer.get_length()))
+        reply_data += renderer.get_data()
 
         return reply_data
 
@@ -158,7 +157,7 @@ class TestXfrinConnection(unittest.TestCase):
             'questions': [example_soa_question],
             'bad_qid': False,
             'response': True,
-            'rcode': rcode.NOERROR(),
+            'rcode': Rcode.NOERROR(),
             'axfr_after_soa': self._create_normal_response_data
             }
 
@@ -189,11 +188,11 @@ class TestXfrinConnection(unittest.TestCase):
         c.close()
 
     def test_init_chclass(self):
-        c = XfrinConnection({}, 'example.com.', rr_class.CH(), TEST_DB_FILE,
+        c = XfrinConnection({}, 'example.com.', RRClass.CH(), TEST_DB_FILE,
                             threading.Event(), TEST_MASTER_IPV4_ADDRINFO)
-        axfrmsg = c._create_query(rr_type.AXFR())
-        self.assertEqual(question_iter(axfrmsg).get_question().get_class(),
-                         rr_class.CH())
+        axfrmsg = c._create_query(RRType.AXFR())
+        self.assertEqual(axfrmsg.get_question()[0].get_class(),
+                         RRClass.CH())
         c.close()
 
     def test_response_with_invalid_msg(self):
@@ -201,41 +200,41 @@ class TestXfrinConnection(unittest.TestCase):
         self.assertRaises(XfrinTestException, self._handle_xfrin_response)
 
     def test_response_without_end_soa(self):
-        self.conn._send_query(rr_type.AXFR())
+        self.conn._send_query(RRType.AXFR())
         self.conn.reply_data = self.conn.create_response_data()
         self.assertRaises(XfrinTestException, self._handle_xfrin_response)
 
     def test_response_bad_qid(self):
-        self.conn._send_query(rr_type.AXFR())
+        self.conn._send_query(RRType.AXFR())
         self.conn.reply_data = self.conn.create_response_data(bad_qid = True)
         self.assertRaises(XfrinException, self._handle_xfrin_response)
 
     def test_response_non_response(self):
-        self.conn._send_query(rr_type.AXFR())
+        self.conn._send_query(RRType.AXFR())
         self.conn.reply_data = self.conn.create_response_data(response = False)
         self.assertRaises(XfrinException, self._handle_xfrin_response)
 
     def test_response_error_code(self):
-        self.conn._send_query(rr_type.AXFR())
+        self.conn._send_query(RRType.AXFR())
         self.conn.reply_data = self.conn.create_response_data(
-            rcode=rcode.SERVFAIL())
+            rcode=Rcode.SERVFAIL())
         self.assertRaises(XfrinException, self._handle_xfrin_response)
 
     def test_response_multi_question(self):
-        self.conn._send_query(rr_type.AXFR())
+        self.conn._send_query(RRType.AXFR())
         self.conn.reply_data = self.conn.create_response_data(
             questions=[example_axfr_question, example_axfr_question])
         self.assertRaises(XfrinException, self._handle_xfrin_response)
 
     def test_response_empty_answer(self):
-        self.conn._send_query(rr_type.AXFR())
+        self.conn._send_query(RRType.AXFR())
         self.conn.reply_data = self.conn.create_response_data(answers=[])
         # Should an empty answer trigger an exception?  Even though it's very
         # unusual it's not necessarily invalid.  Need to revisit.
         self.assertRaises(XfrinException, self._handle_xfrin_response)
 
     def test_response_non_response(self):
-        self.conn._send_query(rr_type.AXFR())
+        self.conn._send_query(RRType.AXFR())
         self.conn.reply_data = self.conn.create_response_data(response = False)
         self.assertRaises(XfrinException, self._handle_xfrin_response)
 
@@ -247,7 +246,7 @@ class TestXfrinConnection(unittest.TestCase):
 
     def test_soacheck_with_bad_response(self):
         self.conn.response_generator = self._create_broken_response_data
-        self.assertRaises(UserWarning, self.conn._check_soa_serial)
+        self.assertRaises(MessageTooShort, self.conn._check_soa_serial)
 
     def test_soacheck_badqid(self):
         self.soa_response_params['bad_qid'] = True
@@ -260,14 +259,14 @@ class TestXfrinConnection(unittest.TestCase):
         self.assertRaises(XfrinException, self.conn._check_soa_serial)
 
     def test_soacheck_error_code(self):
-        self.soa_response_params['rcode'] = rcode.SERVFAIL()
+        self.soa_response_params['rcode'] = Rcode.SERVFAIL()
         self.conn.response_generator = self._create_soa_response_data
         self.assertRaises(XfrinException, self.conn._check_soa_serial)
 
     def test_response_shutdown(self):
         self.conn.response_generator = self._create_normal_response_data
         self.conn._shutdown_event.set()
-        self.conn._send_query(rr_type.AXFR())
+        self.conn._send_query(RRType.AXFR())
         self.assertRaises(XfrinException, self._handle_xfrin_response)
 
     def test_response_timeout(self):
@@ -282,13 +281,13 @@ class TestXfrinConnection(unittest.TestCase):
 
     def test_response_bad_message(self):
         self.conn.response_generator = self._create_broken_response_data
-        self.conn._send_query(rr_type.AXFR())
+        self.conn._send_query(RRType.AXFR())
         self.assertRaises(Exception, self._handle_xfrin_response)
 
     def test_response(self):
         # normal case.
         self.conn.response_generator = self._create_normal_response_data
-        self.conn._send_query(rr_type.AXFR())
+        self.conn._send_query(RRType.AXFR())
         # two SOAs, and only these have been transfered.  the 2nd SOA is just
         # a marker, so only 1 RR has been provided in the iteration.
         self.assertEqual(self._handle_xfrin_response(), 1)
@@ -317,7 +316,10 @@ class TestXfrinConnection(unittest.TestCase):
 
     def test_do_soacheck_broken_response(self):
         self.conn.response_generator = self._create_broken_response_data
-        self.assertEqual(self.conn.do_xfrin(True), XFRIN_FAIL)
+        # XXX: TODO: this test failed here, should xfr not raise an
+        # exception but simply drop and return FAIL?
+        #self.assertEqual(self.conn.do_xfrin(True), XFRIN_FAIL)
+        self.assertRaises(MessageTooShort, self.conn.do_xfrin, True)
 
     def test_do_soacheck_badqid(self):
         # the QID mismatch would internally trigger a XfrinException exception,
@@ -488,12 +490,12 @@ class TestXfrin(unittest.TestCase):
                                                   self.args)['result'][0], 1)
 
     def test_command_handler_retransfer_nomodule(self):
-        dns_module = sys.modules['bind10_dns'] # this must exist
-        del sys.modules['bind10_dns']
+        dns_module = sys.modules['libdns_python'] # this must exist
+        del sys.modules['libdns_python']
         self.assertEqual(self.xfr.command_handler("retransfer",
                                                   self.args)['result'][0], 1)
         # sys.modules is global, so we must recover it
-        sys.modules['bind10_dns'] = dns_module
+        sys.modules['libdns_python'] = dns_module
 
     def test_command_handler_refresh(self):
         # at this level, refresh is no different than retransfer.

+ 46 - 39
src/bin/xfrin/xfrin.py.in

@@ -29,7 +29,7 @@ import random
 from optparse import OptionParser, OptionValueError
 from isc.config.ccsession import *
 try:
-    from bind10_dns import *
+    from libdns_python import *
 except ImportError as e:
     # C++ loadable module may not be installed; even so the xfrin process
     # must keep running, so we warn about it and move forward.
@@ -40,11 +40,14 @@ except ImportError as e:
 # installed on the system
 if "B10_FROM_BUILD" in os.environ:
     SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/xfrin"
+    AUTH_SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/auth"
 else:
     PREFIX = "@prefix@"
     DATAROOTDIR = "@datarootdir@"
     SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
+    AUTH_SPECFILE_PATH = SPECFILE_PATH
 SPECFILE_LOCATION = SPECFILE_PATH + "/xfrin.spec"
+AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + "/auth.spec"
 
 
 __version__ = 'BIND10'
@@ -98,14 +101,13 @@ class XfrinConnection(asyncore.dispatcher):
     def _create_query(self, query_type):
         '''Create dns query message. '''
 
-        msg = message(message_mode.RENDER)
+        msg = Message(Message.RENDER)
         query_id = random.randint(0, 0xFFFF)
         self._query_id = query_id
         msg.set_qid(query_id)
-        msg.set_opcode(op_code.QUERY())
-        msg.set_rcode(rcode.NOERROR())
-        query_question = question(name(self._zone_name), self._rrclass,
-                                  query_type)
+        msg.set_opcode(Opcode.QUERY())
+        msg.set_rcode(Rcode.NOERROR())
+        query_question = Question(Name(self._zone_name), self._rrclass, query_type)
         msg.add_question(query_question)
         return msg
 
@@ -120,13 +122,12 @@ class XfrinConnection(asyncore.dispatcher):
         '''Send query message over TCP. '''
 
         msg = self._create_query(query_type)
-        obuf = output_buffer(0)
-        render = message_render(obuf)
+        render = MessageRenderer()
         msg.to_wire(render)
-        header_len = struct.pack('H', socket.htons(obuf.get_length()))
+        header_len = struct.pack('H', socket.htons(render.get_length()))
 
         self._send_data(header_len)
-        self._send_data(obuf.get_data())
+        self._send_data(render.get_data())
 
     def _asyncore_loop(self):
         '''
@@ -157,12 +158,12 @@ class XfrinConnection(asyncore.dispatcher):
         True: soa serial in master is bigger
         '''
 
-        self._send_query(rr_type.SOA())
+        self._send_query(RRType("SOA"))
         data_len = self._get_request_response(2)
         msg_len = socket.htons(struct.unpack('H', data_len)[0])
         soa_response = self._get_request_response(msg_len)
-        msg = message(message_mode.PARSE)
-        msg.from_wire(input_buffer(soa_response))
+        msg = Message(Message.PARSE)
+        msg.from_wire(soa_response)
 
         # perform some minimal level validation.  It's an open issue how
         # strict we should be (see the comment in _check_response_header())
@@ -181,11 +182,12 @@ class XfrinConnection(asyncore.dispatcher):
             if check_soa:
                 logstr = 'SOA check for \'%s\' ' % self._zone_name
                 ret =  self._check_soa_serial()
-            
+
             logstr = 'transfer of \'%s\': AXFR ' % self._zone_name
             if ret == XFRIN_OK:
                 self.log_msg(logstr + 'started')
-                self._send_query(rr_type.AXFR())
+                # TODO: .AXFR() RRType.AXFR()
+                self._send_query(RRType(252))
                 isc.datasrc.sqlite3_ds.load(self._db_file, self._zone_name,
                                             self._handle_xfrin_response)
 
@@ -226,10 +228,10 @@ class XfrinConnection(asyncore.dispatcher):
         # cause interoperability trouble with stricter checks.
 
         msg_rcode = msg.get_rcode()
-        if msg_rcode != rcode.NOERROR():
+        if msg_rcode != Rcode.NOERROR():
             raise XfrinException('error response: %s' % msg_rcode.to_text())
 
-        if not msg.get_header_flag(message_flag.QR()):
+        if not msg.get_header_flag(MessageFlag.QR()):
             raise XfrinException('response is not a response ')
 
         if msg.get_qid() != self._query_id:
@@ -240,28 +242,24 @@ class XfrinConnection(asyncore.dispatcher):
 
         self._check_response_header(msg)
 
-        if msg.get_rr_count(section.ANSWER()) == 0:
+        if msg.get_rr_count(Section.ANSWER()) == 0:
             raise XfrinException('answer section is empty')
 
-        if msg.get_rr_count(section.QUESTION()) > 1:
+        if msg.get_rr_count(Section.QUESTION()) > 1:
             raise XfrinException('query section count greater than 1')
 
-    def _handle_answer_section(self, rrset_iter):
+    def _handle_answer_section(self, answer_section):
         '''Return a generator for the reponse in one tcp package to a zone transfer.'''
 
-        while not rrset_iter.is_last():
-            rrset = rrset_iter.get_rrset()
-            rrset_iter.next()
+        for rrset in answer_section:
             rrset_name = rrset.get_name().to_text()
             rrset_ttl = int(rrset.get_ttl().to_text())
             rrset_class = rrset.get_class().to_text()
             rrset_type = rrset.get_type().to_text()
 
-            rdata_iter = rrset.get_rdata_iterator()
-            rdata_iter.first()
-            while not rdata_iter.is_last():
+            for rdata in rrset.get_rdata():
                 # Count the soa record count
-                if rrset.get_type() == rr_type.SOA():
+                if rrset.get_type() == RRType("SOA"):
                     self._soa_rr_count += 1
 
                     # XXX: the current DNS message parser can't preserve the
@@ -273,24 +271,22 @@ class XfrinConnection(asyncore.dispatcher):
                         # Avoid inserting soa record twice
                         break
 
-                rdata_text = rdata_iter.get_current().to_text()
+                rdata_text = rdata.to_text()
                 yield (rrset_name, rrset_ttl, rrset_class, rrset_type,
                        rdata_text)
-                rdata_iter.next()
 
     def _handle_xfrin_response(self):
         '''Return a generator for the response to a zone transfer. '''
-
         while True:
             data_len = self._get_request_response(2)
             msg_len = socket.htons(struct.unpack('H', data_len)[0])
             recvdata = self._get_request_response(msg_len)
-            msg = message(message_mode.PARSE)
-            msg.from_wire(input_buffer(recvdata))
+            msg = Message(Message.PARSE)
+            msg.from_wire(recvdata)
             self._check_response_status(msg)
-
-            rrset_iter = section_iter(msg, section.ANSWER())
-            for rr in self._handle_answer_section(rrset_iter):
+            
+            answer_section = msg.get_section(Section.ANSWER())
+            for rr in self._handle_answer_section(answer_section):
                 yield rr
 
             if self._soa_rr_count == 2:
@@ -411,7 +407,7 @@ a separate method for the convenience of unit tests.
                 # The default RR class is IN.  We should fix this so that
                 # the class is passed in the command arg (where we specify
                 # the default)
-                rrclass = rr_class.IN()
+                rrclass = RRClass.IN()
                 zone_name, master_addr, db_file = self._parse_cmd_params(args)
                 ret = self.xfrin_start(zone_name, rrclass, db_file, master_addr,
                                    False if command == 'retransfer' else True)
@@ -441,7 +437,18 @@ a separate method for the convenience of unit tests.
         db_file = args.get('db_file')
         if not db_file:
             #TODO, the db file path should be got in auth server's configuration
-            db_file = '@@LOCALSTATEDIR@@/@PACKAGE@/zone.sqlite3'
+            # if we need access to this configuration more often, we
+            # should add it on start, and not remove it here
+            # (or, if we have writable ds, we might not need this in
+            # the first place)
+            self._cc.add_remote_config(AUTH_SPECFILE_LOCATION)
+            db_file, is_default = self._cc.get_remote_config_value("Auth", "database_file")
+            if is_default and "B10_FROM_BUILD" in os.environ:
+                # this too should be unnecessary, but currently the
+                # 'from build' override isn't stored in the config
+                # (and we don't have writable datasources yet)
+                db_file = os.environ["B10_FROM_BUILD"] + os.sep + "bind10_zones.sqlite3"
+            self._cc.remove_remote_config(AUTH_SPECFILE_LOCATION)
 
         return (zone_name, master_addrinfo, db_file)
 
@@ -451,8 +458,8 @@ a separate method for the convenience of unit tests.
 
     def xfrin_start(self, zone_name, rrclass, db_file, master_addrinfo,
                     check_soa = True):
-        if "bind10_dns" not in sys.modules:
-            return (1, "xfrin failed, can't load dns message python library: 'bind10_dns'")
+        if "libdns_python" not in sys.modules:
+            return (1, "xfrin failed, can't load dns message python library: 'libdns_python'")
 
         # check max_transfer_in, else return quota error
         if self.recorder.count() >= self._max_transfers_in:

+ 1 - 1
src/bin/xfrout/run_b10-xfrout.sh.in

@@ -19,7 +19,7 @@ PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
 export PYTHON_EXEC
 
 MYPATH_PATH=@abs_top_builddir@/src/bin/xfrout
-PYTHONPATH=@abs_top_srcdir@/src/lib/python:@abs_top_builddir@/src/lib/xfr/.libs
+PYTHONPATH=@abs_top_srcdir@/src/lib/python:@abs_top_builddir@/src/lib/xfr/.libs:@abs_top_builddir@/src/lib/dns/python/.libs
 export PYTHONPATH
 
 cd ${MYPATH_PATH}

+ 1 - 5
src/bin/xfrout/tests/Makefile.am

@@ -1,16 +1,12 @@
 PYTESTS = xfrout_test.py
 EXTRA_DIST = $(PYTESTS)
 
-if HAVE_BOOST_PYTHON
-
 # later will have configure option to choose this, like: coverage run --branch
 PYCOVERAGE = $(PYTHON)
 # test using command-line arguments, so use check-local target instead of TESTS
 check-local:
 	for pytest in $(PYTESTS) ; do \
 	echo Running test: $$pytest ; \
-	env PYTHONPATH=$(abs_top_builddir)/src/bin/xfrout:$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/xfr/.libs \
+	env PYTHONPATH=$(abs_top_builddir)/src/bin/xfrout:$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/xfr/.libs \
 	$(PYCOVERAGE) $(abs_srcdir)/$$pytest ; \
 	done
-
-endif

+ 37 - 31
src/bin/xfrout/tests/xfrout_test.py

@@ -19,7 +19,7 @@
 import unittest
 import os
 from isc.cc.session import *
-from bind10_dns import *
+from libdns_python import *
 from xfrout import *
 
 # our fake socket, where we can read and insert messages
@@ -46,8 +46,8 @@ class MySocket():
     
     def read_msg(self):
         sent_data = self.readsent()
-        get_msg = message(message_mode.PARSE)
-        get_msg.from_wire(input_buffer(bytes(sent_data[2:])))
+        get_msg = Message(Message.PARSE)
+        get_msg.from_wire(bytes(sent_data[2:]))
         return get_msg
     
     def clear_send(self):
@@ -69,15 +69,16 @@ class Dbserver:
 
 class TestXfroutSession(unittest.TestCase):
     def getmsg(self):
-        msg = message(message_mode.PARSE)
-        msg.from_wire(input_buffer(self.mdata))
+        msg = Message(Message.PARSE)
+        msg.from_wire(self.mdata)
         return msg
 
     def setUp(self):
         request = MySocket(socket.AF_INET,socket.SOCK_STREAM)
-        self.xfrsess = MyXfroutSession(request, None, None)
+        self.log = isc.log.NSLogger('xfrout', '',  severity = 'critical', log_to_console = False )
+        self.xfrsess = MyXfroutSession(request, None, None, self.log)
         self.xfrsess.server = Dbserver()
-        self.mdata = b'\xd6=\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\x03com\x00\x00\xfc\x00\x01'
+        self.mdata = bytes(b'\xd6=\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\x03com\x00\x00\xfc\x00\x01')
         self.sock = MySocket(socket.AF_INET,socket.SOCK_STREAM)
         self.soa_record = (4, 3, 'example.com.', 'com.example.', 3600, 'SOA', None, 'master.example.com. admin.example.com. 1234 3600 1800 2419200 7200')
 
@@ -96,7 +97,7 @@ class TestXfroutSession(unittest.TestCase):
 
     def test_reply_xfrout_query_with_error_rcode(self):
         msg = self.getmsg()
-        self.xfrsess._reply_query_with_error_rcode(msg, self.sock, rcode(3))
+        self.xfrsess._reply_query_with_error_rcode(msg, self.sock, Rcode(3))
         get_msg = self.sock.read_msg()
         self.assertEqual(get_msg.get_rcode().to_text(), "NXDOMAIN") 
      
@@ -110,7 +111,7 @@ class TestXfroutSession(unittest.TestCase):
         self.assertEqual(msg.get_qid(), qid)
         self.assertEqual(msg.get_opcode(), opcode)
         self.assertEqual(msg.get_rcode(), rcode)
-        self.assertTrue(msg.get_header_flag(message_flag.AA()))
+        self.assertTrue(msg.get_header_flag(MessageFlag.AA()))
 
     def test_reply_query_with_format_error(self):
          
@@ -122,11 +123,10 @@ class TestXfroutSession(unittest.TestCase):
     def test_create_rrset_from_db_record(self):
         rrset = self.xfrsess._create_rrset_from_db_record(self.soa_record)
         self.assertEqual(rrset.get_name().to_text(), "example.com.")
-        self.assertEqual(rrset.get_class(), rr_class.IN())
+        self.assertEqual(rrset.get_class(), RRClass("IN"))
         self.assertEqual(rrset.get_type().to_text(), "SOA")
-        rdata_iter = rrset.get_rdata_iterator()
-        rdata_iter.first()
-        self.assertEqual(rdata_iter.get_current().to_text(), self.soa_record[7])
+        rdata = rrset.get_rdata()
+        self.assertEqual(rdata[0].to_text(), self.soa_record[7])
 
     def test_send_message_with_last_soa(self):
         rrset_soa = self.xfrsess._create_rrset_from_db_record(self.soa_record)
@@ -136,18 +136,17 @@ class TestXfroutSession(unittest.TestCase):
         self.xfrsess._send_message_with_last_soa(msg, self.sock, rrset_soa)
         get_msg = self.sock.read_msg()
 
-        self.assertEqual(get_msg.get_rr_count(section.QUESTION()), 1)
-        self.assertEqual(get_msg.get_rr_count(section.ANSWER()), 1)
-        self.assertEqual(get_msg.get_rr_count(section.AUTHORITY()), 0)
+        self.assertEqual(get_msg.get_rr_count(Section.QUESTION()), 1)
+        self.assertEqual(get_msg.get_rr_count(Section.ANSWER()), 1)
+        self.assertEqual(get_msg.get_rr_count(Section.AUTHORITY()), 0)
 
-        answer_rrset_iter = section_iter(get_msg, section.ANSWER())
-        answer = answer_rrset_iter.get_rrset()
+        #answer_rrset_iter = section_iter(get_msg, section.ANSWER())
+        answer = get_msg.get_section(Section.ANSWER())[0]#answer_rrset_iter.get_rrset()
         self.assertEqual(answer.get_name().to_text(), "example.com.")
-        self.assertEqual(answer.get_class(), rr_class.IN())
+        self.assertEqual(answer.get_class(), RRClass("IN"))
         self.assertEqual(answer.get_type().to_text(), "SOA")
-        rdata_iter = answer.get_rdata_iterator()
-        rdata_iter.first()
-        self.assertEqual(rdata_iter.get_current().to_text(), self.soa_record[7])
+        rdata = answer.get_rdata()
+        self.assertEqual(rdata[0].to_text(), self.soa_record[7])
 
     def test_get_message_len(self):
         msg = self.getmsg()
@@ -205,7 +204,7 @@ class TestXfroutSession(unittest.TestCase):
     def test_dns_xfrout_start_notauth(self):
         self.xfrsess._get_query_zone_name = self.default
         def notauth(formpara):
-            return rcode.NOTAUTH()
+            return Rcode.NOTAUTH()
         self.xfrsess._check_xfrout_available = notauth
         self.xfrsess.dns_xfrout_start(self.sock, self.mdata)
         get_msg = self.sock.read_msg()
@@ -214,7 +213,7 @@ class TestXfroutSession(unittest.TestCase):
     def test_dns_xfrout_start_noerror(self):
         self.xfrsess._get_query_zone_name = self.default
         def noerror(form):
-            return rcode.NOERROR() 
+            return Rcode.NOERROR() 
         self.xfrsess._check_xfrout_available = noerror
         
         def myreply(msg, sock, zonename):
@@ -236,28 +235,35 @@ class TestXfroutSession(unittest.TestCase):
         sqlite3_ds.get_zone_datas = get_zone_datas
         self.xfrsess._reply_xfrout_query(self.getmsg(), self.sock, "example.com.")
         reply_msg = self.sock.read_msg()
-        self.assertEqual(reply_msg.get_rr_count(section.ANSWER()), 2)
+        self.assertEqual(reply_msg.get_rr_count(Section.ANSWER()), 2)
 
-        # set event
-        self.xfrsess.server._shutdown_event.set()
-        self.assertRaises(XfroutException, self.xfrsess._reply_xfrout_query, self.getmsg(), self.sock, "example.com.")
+class MyCCSession():
+    def __init__(self):
+        pass
+
+    def get_remote_config_value(self, module_name, identifier):
+        if module_name == "Auth" and identifier == "database_file":
+            return "initdb.file", False
+        else:
+            return "unknown", False
+    
 
 class MyUnixSockServer(UnixSockServer):
     def __init__(self):
         self._lock = threading.Lock()
         self._transfers_counter = 0
         self._shutdown_event = threading.Event()
-        self._db_file = "initdb.file"
         self._max_transfers_out = 10
+        self._cc = MyCCSession()
+        self._log = isc.log.NSLogger('xfrout', '', severity = 'critical', log_to_console = False )
 
 class TestUnixSockServer(unittest.TestCase):
     def setUp(self):
         self.unix = MyUnixSockServer()
      
     def test_updata_config_data(self):
-        self.unix.update_config_data({'transfers_out':10, 'db_file':"db.file"})
+        self.unix.update_config_data({'transfers_out':10 })
         self.assertEqual(self.unix._max_transfers_out, 10)
-        self.assertEqual(self.unix._db_file, "db.file")
 
     def test_get_db_file(self):
         self.assertEqual(self.unix.get_db_file(), "initdb.file")

+ 117 - 85
src/bin/xfrout/xfrout.py.in

@@ -26,13 +26,15 @@ from isc.datasrc import sqlite3_ds
 from socketserver import *
 import os
 from isc.config.ccsession import *
+from isc.log.log import *
 from isc.cc import SessionError
 import socket
+import select
 import errno
 from optparse import OptionParser, OptionValueError
 try:
-    from bind10_xfr import *
-    from bind10_dns import *
+    from libxfr_python import *
+    from libdns_python import *
 except ImportError as e:
     # C++ loadable module may not be installed; even so the xfrout process
     # must keep running, so we warn about it and move forward.
@@ -40,29 +42,35 @@ except ImportError as e:
 
 if "B10_FROM_BUILD" in os.environ:
     SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/xfrout"
+    AUTH_SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/auth"
+    UNIX_SOCKET_FILE= os.environ["B10_FROM_BUILD"] + "/auth_xfrout_conn"
 else:
     PREFIX = "@prefix@"
     DATAROOTDIR = "@datarootdir@"
     SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
-SPECFILE_LOCATION = SPECFILE_PATH + "/xfrout.spec"
-UNIX_SOCKET_FILE = "@@LOCALSTATEDIR@@/auth_xfrout_conn"
+    AUTH_SPECFILE_PATH = SPECFILE_PATH
+    UNIX_SOCKET_FILE = "@@LOCALSTATEDIR@@/auth_xfrout_conn"
 
+SPECFILE_LOCATION = SPECFILE_PATH + "/xfrout.spec"
+AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + os.sep + "auth.spec"
 MAX_TRANSFERS_OUT = 10
-verbose_mode = False
-
-
-class XfroutException(Exception): pass
-
+VERBOSE_MODE = False
 
 class XfroutSession(BaseRequestHandler):
+    def __init__(self, request, client_address, server, log):
+        # The initializer for the superclass may call functions
+        # that need _log to be set, so we set it first
+        self._log = log
+        BaseRequestHandler.__init__(self, request, client_address, server)
+
     def handle(self):
         fd = recv_fd(self.request.fileno())
+        
         if fd < 0:
             # This may happen when one xfrout process try to connect to
             # xfrout unix socket server, to check whether there is another
             # xfrout running. 
-            print("[b10-xfrout] Failed to receive the FD for XFR connection, "
-                  "maybe because another xfrout process was started.")
+            self._log.log_message("error", "Failed to receive the file descriptor for XFR connection")
             return
 
         data_len = self.request.recv(2)
@@ -72,27 +80,27 @@ class XfroutSession(BaseRequestHandler):
         try:
             self.dns_xfrout_start(sock, msgdata)
         except Exception as e:
-            if verbose_mode:
-                self.log_msg(str(e))
+            self._log.log_message("error", str(e))
 
+        sock.shutdown(socket.SHUT_RDWR)
         sock.close()
+        os.close(fd)
+        pass
 
     def _parse_query_message(self, mdata):
         ''' parse query message to [socket,message]'''
         #TODO, need to add parseHeader() in case the message header is invalid 
         try:
-            msg = message(message_mode.PARSE)
-            msg.from_wire(input_buffer(mdata))
+            msg = Message(Message.PARSE)
+            Message.from_wire(msg, mdata)
         except Exception as err:
-            if verbose_mode:
-                self.log_msg(str(err))
-            return rcode.FORMERR(), None
+            self._log.log_message("error", str(err))
+            return Rcode.FORMERR(), None
 
-        return rcode.NOERROR(), msg
+        return Rcode.NOERROR(), msg
 
     def _get_query_zone_name(self, msg):
-        q_iter = question_iter(msg)
-        question = q_iter.get_question()
+        question = msg.get_question()[0]
         return question.get_name().to_text()
 
 
@@ -105,12 +113,14 @@ class XfroutSession(BaseRequestHandler):
 
 
     def _send_message(self, sock, msg):
-        obuf = output_buffer(0)
-        render = message_render(obuf)
+        #obuf = output_buffer(0)
+        #render = message_render(obuf)
+        render = MessageRenderer()
+        render.set_length_limit(65535)
         msg.to_wire(render)
-        header_len = struct.pack('H', socket.htons(obuf.get_length()))
+        header_len = struct.pack('H', socket.htons(render.get_length()))
         self._send_data(sock, header_len)
-        self._send_data(sock, obuf.get_data())
+        self._send_data(sock, render.get_data())
 
 
     def _reply_query_with_error_rcode(self, msg, sock, rcode_):
@@ -125,7 +135,7 @@ class XfroutSession(BaseRequestHandler):
             return # query message is invalid. send nothing back. 
 
         msg.make_response()
-        msg.set_rcode(rcode.FORMERR())
+        msg.set_rcode(Rcode.FORMERR())
         self._send_message(sock, msg)
 
 
@@ -150,40 +160,37 @@ class XfroutSession(BaseRequestHandler):
            eg. check allow_transfer setting, 
         '''
         if not self._zone_exist(zone_name):
-            return rcode.NOTAUTH()
+            return Rcode.NOTAUTH()
 
         if self._zone_is_empty(zone_name):
-            return rcode.SERVFAIL() 
+            return Rcode.SERVFAIL() 
 
         #TODO, check allow_transfer
         if not self.server.increase_transfers_counter():
-            return rcode.REFUSED()
+            return Rcode.REFUSED()
 
-        return rcode.NOERROR()
+        return Rcode.NOERROR()
 
 
     def dns_xfrout_start(self, sock, msg_query):
         rcode_, msg = self._parse_query_message(msg_query)
         #TODO. create query message and parse header
-        if rcode_ != rcode.NOERROR():
+        if rcode_ != Rcode.NOERROR():
             return self._reply_query_with_format_error(msg, sock)
 
         zone_name = self._get_query_zone_name(msg)
         rcode_ = self._check_xfrout_available(zone_name)
-        if rcode_ != rcode.NOERROR():
+        if rcode_ != Rcode.NOERROR():
+            self._log.log_message("info", "transfer of '%s/IN' failed: %s",
+                                  zone_name, rcode_.to_text())
             return self. _reply_query_with_error_rcode(msg, sock, rcode_)
 
         try:
-            if verbose_mode:
-                self.log_msg("transfer of '%s/IN': AXFR started" % zone_name)
-
+            self._log.log_message("info", "transfer of '%s/IN': AXFR started" % zone_name)
             self._reply_xfrout_query(msg, sock, zone_name)
-
-            if verbose_mode:
-                self.log_msg("transfer of '%s/IN': AXFR end" % zone_name)
+            self._log.log_message("info", "transfer of '%s/IN': AXFR end" % zone_name)
         except Exception as err:
-            if verbose_mode:
-                sys.stderr.write("[b10-xfrout] %s\n" % str(err))
+            self._log.log_message("error", str(err))
 
         self.server.decrease_transfers_counter()
         return    
@@ -194,21 +201,21 @@ class XfroutSession(BaseRequestHandler):
         opcode = msg.get_opcode()
         rcode = msg.get_rcode()
         
-        msg.clear(message_mode.RENDER)
+        msg.clear(Message.RENDER)
         msg.set_qid(qid)
         msg.set_opcode(opcode)
         msg.set_rcode(rcode)
-        msg.set_header_flag(message_flag.AA())
-        msg.set_header_flag(message_flag.QR())
+        msg.set_header_flag(MessageFlag.AA())
+        msg.set_header_flag(MessageFlag.QR())
         return msg
 
     def _create_rrset_from_db_record(self, record):
         '''Create one rrset from one record of datasource, if the schema of record is changed, 
         This function should be updated first.
         '''
-        rrtype_ = rr_type(record[5])
-        rdata_ = create_rdata(rrtype_, rr_class.IN(), " ".join(record[7:]))
-        rrset_ = rrset(name(record[2]), rr_class.IN(), rrtype_, rr_ttl( int(record[4])))
+        rrtype_ = RRType(record[5])
+        rdata_ = Rdata(rrtype_, RRClass("IN"), " ".join(record[7:]))
+        rrset_ = RRset(Name(record[2]), RRClass("IN"), rrtype_, RRTTL( int(record[4])))
         rrset_.add_rdata(rdata_)
         return rrset_
          
@@ -217,20 +224,19 @@ class XfroutSession(BaseRequestHandler):
         added, a new message should be created to send out the last soa .
         '''
 
-        obuf = output_buffer(0)
-        render = message_render(obuf)
+        render = MessageRenderer()
         msg.to_wire(render)
-        old_message_len = obuf.get_length()
-        msg.add_rrset(section.ANSWER(), rrset_soa)
+        old_message_len = render.get_length()
+        msg.add_rrset(Section.ANSWER(), rrset_soa)
 
         msg.to_wire(render)
-        message_len = obuf.get_length()
+        message_len = render.get_length()
 
         if message_len != old_message_len:
             self._send_message(sock, msg)
         else:
             msg = self._clear_message(msg)
-            msg.add_rrset(section.ANSWER(), rrset_soa)
+            msg.add_rrset(Section.ANSWER(), rrset_soa)
             self._send_message(sock, msg)
 
     def _get_message_len(self, msg):
@@ -238,59 +244,62 @@ class XfroutSession(BaseRequestHandler):
         a better way, I need check with jinmei later.
         '''
 
-        obuf = output_buffer(0)
-        render = message_render(obuf)
+        render = MessageRenderer()
         msg.to_wire(render)
-        return obuf.get_length()
+        return render.get_length()
 
 
     def _reply_xfrout_query(self, msg, sock, zone_name):
         #TODO, there should be a better way to insert rrset.
         msg.make_response()
-        msg.set_header_flag(message_flag.AA())
+        msg.set_header_flag(MessageFlag.AA())
         soa_record = sqlite3_ds.get_zone_soa(zone_name, self.server.get_db_file())
         rrset_soa = self._create_rrset_from_db_record(soa_record)
-        msg.add_rrset(section.ANSWER(), rrset_soa)
+        msg.add_rrset(Section.ANSWER(), rrset_soa)
 
         old_message_len = 0
         # TODO, Since add_rrset() return nothing when rrset can't be added, so I have to compare
         # the message length to know if the rrset has been added sucessfully.
         for rr_data in sqlite3_ds.get_zone_datas(zone_name, self.server.get_db_file()):
             if  self.server._shutdown_event.is_set(): # Check if xfrout is shutdown
-                raise XfroutException("shutdown!")
+                self._log.log_message("error", "shutdown!")
 
-            if rr_type(rr_data[5]) == rr_type.SOA(): #ignore soa record
+            # TODO: RRType.SOA() ?
+            if RRType(rr_data[5]) == RRType("SOA"): #ignore soa record
                 continue
-           
+
             rrset_ = self._create_rrset_from_db_record(rr_data)
-            msg.add_rrset(section.ANSWER(), rrset_)
-            message_len = self._get_message_len(msg) 
+            msg.add_rrset(Section.ANSWER(), rrset_)
+            message_len = self._get_message_len(msg)
             if message_len != old_message_len:
                 old_message_len = message_len
                 continue
 
             self._send_message(sock, msg)
             msg = self._clear_message(msg)
-            msg.add_rrset(section.ANSWER(), rrset_) # Add the rrset to the new message
+            msg.add_rrset(Section.ANSWER(), rrset_) # Add the rrset to the new message
             old_message_len = 0
 
         self._send_message_with_last_soa(msg, sock, rrset_soa)
 
-    def log_msg(self, msg):
-        print('[b10-xfrout] ', msg)
-   
 
 class UnixSockServer(ThreadingUnixStreamServer):
     '''The unix domain socket server which accept xfr query sent from auth server.'''
 
-    def __init__(self, sock_file, handle_class, shutdown_event, config_data):
+    def __init__(self, sock_file, handle_class, shutdown_event, config_data, cc, log):
         self._remove_unused_sock_file(sock_file)
         self._sock_file = sock_file
         ThreadingUnixStreamServer.__init__(self, sock_file, handle_class)
         self._lock = threading.Lock()
         self._transfers_counter = 0
         self._shutdown_event = shutdown_event
+        self._log = log
         self.update_config_data(config_data)
+        self._cc = cc
+
+    def finish_request(self, request, client_address):
+        '''Finish one request by instantiating RequestHandlerClass.'''
+        self.RequestHandlerClass(request, client_address, self, self._log)
 
     def _remove_unused_sock_file(self, sock_file):
         '''Try to remove the socket file. If the file is being used 
@@ -327,23 +336,28 @@ class UnixSockServer(ThreadingUnixStreamServer):
         ThreadingUnixStreamServer.shutdown(self)
         try:
             os.unlink(self._sock_file)
-        except:
-            pass
+        except Exception as e:
+            self._log.log_message("error", str(e))
 
     def update_config_data(self, new_config):
         '''Apply the new config setting of xfrout module. '''
-
+        self._log.log_message('info', 'update config data start.')
         self._lock.acquire()
         self._max_transfers_out = new_config.get('transfers_out')
-        self._db_file = new_config.get('db_file')
+        self._log.log_message('info', 'max transfer out : %d', self._max_transfers_out)
         self._lock.release()
+        self._log.log_message('info', 'update config data complete.')
 
     def get_db_file(self):
-        self._lock.acquire()
-        file = self._db_file
-        self._lock.release()
+        file, is_default = self._cc.get_remote_config_value("Auth", "database_file")
+        # this too should be unnecessary, but currently the
+        # 'from build' override isn't stored in the config
+        # (and we don't have indirect python access to datasources yet)
+        if is_default and "B10_FROM_BUILD" in os.environ:
+            file = os.environ["B10_FROM_BUILD"] + os.sep + "bind10_zones.sqlite3"
         return file
 
+
     def increase_transfers_counter(self):
         '''Return False, if counter + 1 > max_transfers_out, or else
         return True
@@ -362,29 +376,45 @@ class UnixSockServer(ThreadingUnixStreamServer):
         self._lock.release()
 
 def listen_on_xfr_query(unix_socket_server):
-
     '''Listen xfr query in one single thread. Polls for shutdown 
     every 0.1 seconds, is there a better time?
     '''
-    unix_socket_server.serve_forever(poll_interval = 0.1)
+
+    while True:
+        try:
+            unix_socket_server.serve_forever(poll_interval = 0.1)
+        except select.error as err:
+            # serve_forever() calls select.select(), which can be 
+            # interrupted.
+            # If it is interrupted, it raises select.error with the 
+            # errno set to EINTR. We ignore this case, and let the
+            # normal program flow continue by trying serve_forever()
+            # again.
+            if err.args[0] != errno.EINTR: raise
+
    
 
 class XfroutServer:
     def __init__(self):
         self._unix_socket_server = None
+        self._log = None
         self._listen_sock_file = UNIX_SOCKET_FILE 
         self._shutdown_event = threading.Event()
         self._cc = isc.config.ModuleCCSession(SPECFILE_LOCATION, self.config_handler, self.command_handler)
+        self._cc.add_remote_config(AUTH_SPECFILE_LOCATION);
         self._config_data = self._cc.get_full_config()
         self._cc.start()
+        self._log = isc.log.NSLogger(self._config_data.get('log_name'), self._config_data.get('log_file'),
+                                self._config_data.get('log_severity'), self._config_data.get('log_versions'),
+                                self._config_data.get('log_max_bytes'), True)
         self._start_xfr_query_listener()
 
-
     def _start_xfr_query_listener(self):
         '''Start a new thread to accept xfr query. '''
     
         self._unix_socket_server = UnixSockServer(self._listen_sock_file, XfroutSession, 
-                                                  self._shutdown_event, self._config_data);
+                                                  self._shutdown_event, self._config_data,
+                                                  self._cc, self._log);
         listener = threading.Thread(target = listen_on_xfr_query, args = (self._unix_socket_server,))
         listener.start()
 
@@ -398,6 +428,9 @@ class XfroutServer:
                 continue
             self._config_data[key] = new_config[key]
         
+        if self._log:
+            self._log.update_config(new_config)
+
         if self._unix_socket_server:
             self._unix_socket_server.update_config_data(self._config_data)
 
@@ -423,8 +456,7 @@ class XfroutServer:
 
     def command_handler(self, cmd, args):
         if cmd == "shutdown":
-            if verbose_mode:
-                print("[b10-xfrout] Received shutdown command")
+            self._log.log_message("info", "Received shutdown command.")
             self.shutdown()
             answer = create_answer(0)
         else: 
@@ -459,18 +491,18 @@ if '__main__' == __name__:
         parser = OptionParser()
         set_cmd_options(parser)
         (options, args) = parser.parse_args()
-        verbose_mode = options.verbose
+        VERBOSE_MODE = options.verbose
 
         set_signal_handler()
         xfrout_server = XfroutServer()
         xfrout_server.run()
     except KeyboardInterrupt:
-        print("[b10-xfrout] exit xfrout process")
+        sys.stderr.write("[b10-xfrout] exit xfrout process")
     except SessionError as e:
-        print('[b10-xfrout] Error creating xfrout, '
-              'is the command channel daemon running?' )
+        sys.stderr.write("[b10-xfrout] Error creating xfrout," 
+                           "is the command channel daemon running?")
     except ModuleCCSessionError as e:
-        print('[b10-xfrout] exit xfrout process:', e)
+        sys.stderr.write("info", '[b10-xfrout] exit xfrout process:', e)
 
     if xfrout_server:
         xfrout_server.shutdown()

+ 31 - 1
src/bin/xfrout/xfrout.spec.pre.in

@@ -12,7 +12,37 @@
          "item_name": "db_file",
          "item_type": "string",
          "item_optional": False,
-         "item_default": '@@LOCALSTATEDIR@@/@PACKAGE@/zone.sqlite3'
+         "item_default": "@@LOCALSTATEDIR@@/@PACKAGE@/zone.sqlite3"
+       },
+       {
+         "item_name": "log_name",
+         "item_type": "string",
+         "item_optional": False,
+         "item_default": "Xfrout"
+       },
+       {
+         "item_name": "log_file",
+    	 "item_type": "string",
+         "item_optional": False,
+         "item_default": "@@LOCALSTATEDIR@@/@PACKAGE@/log/Xfrout.log"
+       },
+       {
+         "item_name": "log_severity",
+    	 "item_type": "string",
+         "item_optional": False,
+    	 "item_default": "debug"
+       },
+       {
+         "item_name": "log_versions",
+    	 "item_type": "integer",
+         "item_optional": False,
+    	 "item_default": 5
+       },
+       {
+         "item_name": "log_max_bytes",
+    	 "item_type": "integer",
+         "item_optional": False,
+    	 "item_default": 1048576
        }
       ],
       "commands": [

+ 1 - 4
src/lib/Makefile.am

@@ -1,4 +1 @@
-SUBDIRS = exceptions dns cc config datasrc python
-if HAVE_BOOST_PYTHON
-SUBDIRS += xfr
-endif
+SUBDIRS = exceptions dns cc config datasrc python xfr

+ 6 - 4
src/lib/cc/Makefile.am

@@ -6,10 +6,12 @@ AM_CXXFLAGS = $(B10_CXXFLAGS)
 # error.  Unfortunately there doesn't seem to be an easy way to selectively
 # avoid the error.  As a short term workaround we suppress this warning
 # for the entire this module.  See also src/bin/auth/Makefile.am.
+if USE_GXX
 AM_CXXFLAGS += -Wno-unused-parameter
+endif
 
-lib_LIBRARIES = libcc.a
-libcc_a_SOURCES = data.cc data.h session.cc session.h
+lib_LTLIBRARIES = libcc.la
+libcc_la_SOURCES = data.cc data.h session.cc session.h
 
 CLEANFILES = *.gcno *.gcda session_config.h
 
@@ -27,8 +29,8 @@ run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 # TODO: remove PTHREAD_LDFLAGS (and from configure too)
 run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS) $(PTHREAD_LDFLAGS)
 
-run_unittests_LDADD = libcc.a $(GTEST_LDADD)
-run_unittests_LDADD +=  $(top_builddir)/src/lib/dns/.libs/libdns.a
+run_unittests_LDADD = libcc.la $(GTEST_LDADD)
+run_unittests_LDADD +=  $(top_builddir)/src/lib/dns/.libs/libdns++.a
 run_unittests_LDADD +=  $(top_builddir)/src/lib/exceptions/.libs/libexceptions.a
 
 endif

+ 11 - 4
src/lib/cc/session.cc

@@ -19,6 +19,17 @@
 
 #include <stdint.h>
 
+// XXX: there seems to be a strange dependency between ASIO and std library
+// definitions.  On some platforms if we include std headers before ASIO
+// headers unexpected behaviors will happen.
+// A middle term solution is to generalize our local wrapper interface
+// (currently only available for the auth server), where all such portability
+// issues are hidden, and to have other modules use the wrapper.
+#include <unistd.h>             // for some IPC/network system calls
+#include <asio.hpp>
+#include <asio/error_code.hpp>
+#include <asio/system_error.hpp>
+
 #include <cstdio>
 #include <vector>
 #include <iostream>
@@ -29,10 +40,6 @@
 #include <boost/bind.hpp>
 #include <boost/function.hpp>
 
-#include <asio.hpp>
-#include <asio/error_code.hpp>
-#include <asio/system_error.hpp>
-
 #include <exceptions/exceptions.h>
 
 #include "data.h"

+ 4 - 1
src/lib/cc/session_unittests.cc

@@ -15,10 +15,13 @@
 // $Id: data_unittests.cc 1899 2010-05-21 12:03:59Z jelte $
 
 #include "config.h"
+
+// XXX: the ASIO header must be included before others.  See session.cc.
+#include <asio.hpp>
+
 #include <gtest/gtest.h>
 #include <session.h>
 
-#include <asio.hpp>
 #include <exceptions/exceptions.h>
 
 using namespace isc::cc;

+ 5 - 0
src/lib/config/tests/Makefile.am

@@ -2,7 +2,9 @@ AM_CPPFLAGS = -I$(top_srcdir)/src/lib
 
 AM_CXXFLAGS = $(B10_CXXFLAGS)
 # see src/lib/cc/Makefile.am for -Wno-unused-parameter
+if USE_GXX
 AM_CXXFLAGS += -Wno-unused-parameter
+endif
 
 CLEANFILES = *.gcno *.gcda
 
@@ -20,6 +22,9 @@ run_unittests_LDADD =  $(GTEST_LDADD)
 run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libexceptions.la
 run_unittests_LDADD += libfake_session.la
 run_unittests_LDADD += $(top_builddir)/src/lib/config/libcfgclient.la
+# link *only* to data.o from lib/cc (more importantly, don't link in
+# the session class provided there, since we use our own fake_session
+# here)
 run_unittests_LDADD += $(top_builddir)/src/lib/cc/data.o
 
 endif

+ 0 - 0
src/lib/datasrc/data_source.cc


Some files were not shown because too many files changed in this diff