Browse Source

[1651] Merge branch 'master' into trac1651

Conflicts:
	ChangeLog
	doc/guide/bind10-guide.html
	src/bin/dhcp4/main.cc
	src/lib/dhcp/tests/iface_mgr_unittest.cc
Tomek Mrugalski 13 years ago
parent
commit
7e16a5a50d
100 changed files with 6707 additions and 1405 deletions
  1. 6 0
      .gitignore
  2. 21 0
      AUTHORS
  3. 106 6
      ChangeLog
  4. 22 0
      Makefile.am
  5. 4 7
      README
  6. 11 6
      compatcheck/Makefile.am
  7. 19 3
      configure.ac
  8. 1 1
      doc/Doxyfile
  9. 4 3
      doc/guide/Makefile.am
  10. 380 80
      doc/guide/bind10-guide.html
  11. 566 272
      doc/guide/bind10-guide.txt
  12. 422 20
      doc/guide/bind10-guide.xml
  13. 585 32
      doc/guide/bind10-messages.html
  14. 765 31
      doc/guide/bind10-messages.xml
  15. 5 0
      src/Makefile.am
  16. 4 3
      src/bin/auth/Makefile.am
  17. 52 143
      src/bin/auth/auth_config.cc
  18. 14 0
      src/bin/auth/auth_messages.mes
  19. 171 30
      src/bin/auth/auth_srv.cc
  20. 46 24
      src/bin/auth/auth_srv.h
  21. 18 6
      src/bin/auth/b10-auth.8
  22. 16 9
      src/bin/auth/b10-auth.xml
  23. 6 2
      src/bin/auth/benchmarks/query_bench.cc
  24. 7 4
      src/bin/auth/command.cc
  25. 19 1
      src/bin/auth/common.cc
  26. 14 0
      src/bin/auth/common.h
  27. 5 2
      src/bin/auth/main.cc
  28. 2 1
      src/bin/auth/spec_config.h.pre.in
  29. 9 6
      src/bin/auth/tests/Makefile.am
  30. 305 51
      src/bin/auth/tests/auth_srv_unittest.cc
  31. 77 25
      src/bin/auth/tests/command_unittest.cc
  32. 38 12
      src/bin/auth/tests/common_unittest.cc
  33. 99 25
      src/bin/auth/tests/config_unittest.cc
  34. 0 4
      src/bin/auth/tests/query_unittest.cc
  35. 10 63
      src/bin/bind10/bind10.8
  36. 21 1
      src/bin/bind10/bind10.xml
  37. 9 5
      src/bin/bind10/bind10_messages.mes
  38. 31 3
      src/bin/bind10/bind10_src.py.in
  39. 1 0
      src/bin/bind10/tests/Makefile.am
  40. 53 6
      src/bin/bind10/tests/bind10_test.py.in
  41. 7 8
      src/bin/bindctl/bindctl.1
  42. 10 9
      src/bin/bindctl/bindctl.xml
  43. 0 1
      src/bin/bindctl/command_sets.py
  44. 17 13
      src/bin/bindctl/tests/bindctl_test.py
  45. 13 12
      src/bin/cfgmgr/b10-cfgmgr.8
  46. 29 4
      src/bin/cfgmgr/b10-cfgmgr.py.in
  47. 28 14
      src/bin/cfgmgr/b10-cfgmgr.xml
  48. 17 7
      src/bin/cfgmgr/tests/b10-cfgmgr_test.py.in
  49. 1 0
      src/bin/cmdctl/tests/Makefile.am
  50. 5 0
      src/bin/cmdctl/tests/cmdctl_test.py
  51. 17 20
      src/bin/dbutil/b10-dbutil.8
  52. 25 21
      src/bin/dbutil/b10-dbutil.xml
  53. 1 1
      src/bin/dbutil/dbutil.py.in
  54. 1 0
      src/bin/dbutil/tests/Makefile.am
  55. 36 25
      src/bin/ddns/b10-ddns.8
  56. 51 15
      src/bin/ddns/b10-ddns.xml
  57. 444 30
      src/bin/ddns/ddns.py.in
  58. 19 5
      src/bin/ddns/ddns.spec
  59. 157 3
      src/bin/ddns/ddns_messages.mes
  60. 1 0
      src/bin/ddns/tests/Makefile.am
  61. 935 12
      src/bin/ddns/tests/ddns_test.py
  62. 9 5
      src/bin/dhcp4/Makefile.am
  63. 20 7
      src/bin/dhcp4/main.cc
  64. 24 2
      src/bin/dhcp4/tests/Makefile.am
  65. 170 0
      src/bin/dhcp4/tests/dhcp4_test.py
  66. 13 9
      src/bin/dhcp6/Makefile.am
  67. 1 1
      src/bin/dhcp6/dhcp6_srv.cc
  68. 0 10
      src/bin/dhcp6/interfaces.txt
  69. 17 6
      src/bin/dhcp6/main.cc
  70. 14 9
      src/bin/dhcp6/tests/Makefile.am
  71. 16 20
      src/bin/dhcp6/tests/dhcp6_srv_unittest.cc
  72. 126 28
      src/bin/dhcp6/tests/dhcp6_test.py
  73. 1 0
      src/bin/msgq/msgq.py.in
  74. 12 0
      src/bin/msgq/tests/msgq_test.py
  75. 29 29
      src/bin/resolver/resolver_messages.mes
  76. 3 0
      src/bin/resolver/tests/Makefile.am
  77. 1 1
      src/bin/sockcreator/Makefile.am
  78. 3 0
      src/bin/sockcreator/tests/Makefile.am
  79. 3 2
      src/bin/stats/b10-stats-httpd.8
  80. 8 6
      src/bin/stats/b10-stats-httpd.xml
  81. 11 10
      src/bin/stats/b10-stats.8
  82. 17 18
      src/bin/stats/b10-stats.xml
  83. 1 1
      src/bin/stats/stats.py.in
  84. 1 1
      src/bin/stats/stats_httpd.py.in
  85. 16 16
      src/bin/stats/stats_httpd_messages.mes
  86. 13 13
      src/bin/stats/stats_messages.mes
  87. 1 0
      src/bin/stats/tests/Makefile.am
  88. 4 3
      src/bin/tests/process_rename_test.py.in
  89. 188 2
      src/bin/xfrin/tests/xfrin_test.py
  90. 16 4
      src/bin/xfrin/xfrin.py.in
  91. 9 0
      src/bin/xfrin/xfrin_messages.mes
  92. 52 8
      src/bin/xfrout/tests/xfrout_test.py.in
  93. 43 20
      src/bin/xfrout/xfrout.py.in
  94. 88 77
      src/bin/xfrout/xfrout_messages.mes
  95. 3 1
      src/bin/zonemgr/tests/zonemgr_test.py
  96. 6 5
      src/bin/zonemgr/zonemgr.py.in
  97. 4 4
      src/bin/zonemgr/zonemgr_messages.mes
  98. 3 0
      src/lib/acl/tests/Makefile.am
  99. 3 0
      src/lib/asiodns/tests/Makefile.am
  100. 0 0
      src/lib/asiolink/io_endpoint.cc

+ 6 - 0
.gitignore

@@ -1,3 +1,6 @@
+*.gcda
+*.gcno
+*.gcov
 *.la
 *.lo
 *.o
@@ -28,4 +31,7 @@ TAGS
 /py-compile
 /stamp-h1
 
+/all.info
+/coverage-cpp-html
 /dns++.pc
+/report.info

+ 21 - 0
AUTHORS

@@ -0,0 +1,21 @@
+Chen Zhengzhang
+Dmitriy Volodin
+Evan Hunt
+Haidong Wang
+Haikuo Zhang
+Han Feng
+Jelte Jansen
+Jeremy C. Reed
+Xie Jiagui
+Jin Jian
+JINMEI Tatuya
+Kazunori Fujiwara
+Michael Graff
+Michal Vaner
+Mukund Sivaraman
+Naoki Kambe
+Shane Kerr
+Shen Tingting
+Stephen Morris
+Yoshitaka Aharen
+Zhang Likun

+ 106 - 6
ChangeLog

@@ -1,9 +1,108 @@
-4XX.	[func]*		tomek
+450.	[func]*		tomek
 	b10-dhcp4: DHCPv4 server component is now integrated into
 	BIND10 framework. It can be started from BIND10 (using bindctl)
 	and can receive commands. The only supported command for now
 	is 'Dhcp4 shutdown'.
 
+bind10-devel-20120621 released on June 21. 2012
+
+449.	[bug]		muks
+	b10-xfin: fixed a bug where xfrin sent the wrong notification
+	message to zonemgr on successful zone transfer. This also
+	solves other reported problems such as too frequent attempts
+	of zone refreshing (see Trac #1786 and #1834).
+	(Trac #2023, git b5fbf8a408a047a2552e89ef435a609f5df58d8c)
+
+448.	[func]		team
+	b10-ddns is now functional and handles dynamic update requests
+	per RFC 2136.  See BIND 10 guide for configuration and operation
+	details.
+	(Multiple Trac tickets)
+
+447.	[bug]		jinmei
+	Fixed a bug in b10-xfrout where a helper thread could fall into
+	an infinite loop if b10-auth stops while the thread is waiting for
+	forwarded requests from b10-auth.
+	(Trac #988 and #1833, git 95a03bbefb559615f3f6e529d408b749964d390a)
+
+446.	[bug]		muks
+	A number of warnings reported by Python about unclosed file and
+	socket objects were fixed. Some related code was also made safer.
+	(Trac #1828, git 464682a2180c672f1ed12d8a56fd0a5ab3eb96ed)
+
+445.	[bug]*		jinmei
+	The pre-install check for older SQLite3 DB now refers to the DB
+	file with the prefix of DESTDIR.  This ensures that 'make install'
+	with specific DESTDIR works regardless of the version of the DB
+	file installed in the default path.
+	(Trac #1982, git 380b3e8ec02ef45555c0113ee19329fe80539f71)
+
+444.	[bug]		jinmei
+	libdatasrc: fixed ZoneFinder for database-based data sources so
+	that it handles type DS query correctly, i.e., treating it as
+	authoritative data even on a delegation point.
+	(Trac #1912, git 7130da883f823ce837c10cbf6e216a15e1996e5d)
+
+443.	[func]*		muks
+	The logger now uses a lockfile named `logger_lockfile' that is
+	created in the local state directory to mutually separate
+	individual logging operations from various processes. This is
+	done so that log messages from different processes don't mix
+	together in the middle of lines. The `logger_lockfile` is created
+	with file permission mode 0660. BIND 10's local state directory
+	should be writable and perhaps have g+s mode bit so that the
+	`logger_lockfile` can be opened by a group of processes.
+	(Trac #1704, git ad8d445dd0ba208107eb239405166c5c2070bd8b)
+
+442.	[func]		tomek
+	b10-dhcp4, b10-dhcp6: Both DHCP servers now accept -p parameter
+	that can be used to specify listening port number. This capability
+	is useful only for testing purposes.
+	(Trac #1503, git e60af9fa16a6094d2204f27c40a648fae313bdae)
+
+441.	[func]		tomek
+	libdhcp++: Stub interface detection (support for interfaces.txt
+	file) was removed.
+	(Trac #1281, git 900fc8b420789a8c636bcf20fdaffc60bc1041e0)
+
+bind10-devel-20120517 released on May 17. 2012
+
+440.	[func]		muks
+	bindctl: improved some error messages so they will be more
+	helpful.  Those include the one when the zone name is unspecified
+	or the name is invalid in the b10-auth configuration.
+	(Trac #1627, git 1a4d0ae65b2c1012611f4c15c5e7a29d65339104)
+
+439.	[func]		team
+	The in-memory data source can now load zones from the
+	sqlite3 data source, so that zones stored in the database
+	(and updated for example by xfrin) can be served from memory.
+	(Trac #1789,#1790,#1792,#1793,#1911,
+	git 93f11d2a96ce4dba9308889bdb9be6be4a765b27)
+
+438.	[bug]		naokikambe
+	b10-stats-httpd now sends the system a notification that
+	it is shutting down if it encounters a fatal error during
+	startup.
+	(Trac #1852, git a475ef271d4606f791e5ed88d9b8eb8ed8c90ce6)
+
+437.	[build]		jinmei
+	Building BIND 10 may fail on MacOS if Python has been
+	installed via Homebrew unless --without-werror is specified.
+	The configure script now includes a URL that explains this
+	issue when it detects failure that is possibly because of
+	this problem.
+	(Trac #1907, git 0d03b06138e080cc0391fb912a5a5e75f0f97cec)
+
+436.	[bug]		jelte
+	The --config-file option now works correctly with relative paths if
+	--data-path is not given.
+	(Trac #1889, git ce7d1aef2ca88084e4dacef97132337dd3e50d6c)
+
+435.	[func]		team
+	The in-memory datasource now supports NSEC-signed zones.
+	(Trac #1802-#1810, git 2f9aa4a553a05aa1d9eac06f1140d78f0c99408b)
+
 434.	[func]		tomek
 	libdhcp++: Linux interface detection refactored. The code is
 	now cleaner. Tests better support certain versions of ifconfig.
@@ -37,8 +136,8 @@
 	(Trac #1843, git 551657702a4197ef302c567b5c0eaf2fded3e121)
 
 428.	[bug]		marcin
-	perfdhcp: bind to local address to allow reception of replies from IPv6
-	DHCP servers.
+	perfdhcp: bind to local address to allow reception of
+	replies from IPv6 DHCP servers.
 	(Trac #1908, git 597e059afaa4a89e767f8f10d2a4d78223af3940)
 
 427.	[bug]		jinmei
@@ -48,10 +147,11 @@
 	now manipulates them in the separate table for the NSEC3 namespace.
 	As a result b10-xfrin now correctly updates NSEC3-signed zones by
 	inbound zone transfers.
-	(Trac #1891, git 672f129700dae33b701bb02069cf276238d66be3)
+	(Trac #1781,#1788,#1891, git 672f129700dae33b701bb02069cf276238d66be3)
 
 426.	[bug]		vorner
-	The NSEC3 records are now included when transferring a signed zone out.
+	The NSEC3 records are now included when transferring a
+	signed zone out.
 	(Trac #1782, git 36efa7d10ecc4efd39d2ce4dfffa0cbdeffa74b0)
 
 425.	[func]*		muks
@@ -202,7 +302,7 @@ bind10-devel-20120329 released on March 29, 2012
 	providing result for random instance.
 	(Trac #1751, git 3285353a660e881ec2b645e1bc10d94e5020f357)
 
-403.	[build]*	jelte
+403.	[build]*		jelte
 	The configure option for botan (--with-botan=PATH) is replaced by
 	--with-botan-config=PATH, which takes a full path to a botan-config
 	script, instead of the botan 'install' directory. Also, if not

+ 22 - 0
Makefile.am

@@ -16,6 +16,26 @@ DISTCHECK_CONFIGURE_FLAGS = --disable-install-configurations
 # Use same --with-gtest flag if set
 DISTCHECK_CONFIGURE_FLAGS += $(DISTCHECK_GTEST_CONFIGURE_FLAG)
 
+dist_doc_DATA = AUTHORS COPYING ChangeLog README
+
+.PHONY: check-valgrind check-valgrind-suppress
+
+check-valgrind:
+if HAVE_VALGRIND
+	@VALGRIND_COMMAND="$(VALGRIND) -q --gen-suppressions=all --track-origins=yes --num-callers=48 --leak-check=full --fullpath-after=" \
+	make -C $(abs_top_builddir) check
+else
+	@echo "*** Valgrind is required for check-valgrind ***"; exit 1;
+endif
+
+check-valgrind-suppress:
+if HAVE_VALGRIND
+	@VALGRIND_COMMAND="$(VALGRIND) -q --gen-suppressions=all --error-exitcode=1 --suppressions=$(abs_top_srcdir)/src/valgrind-suppressions --suppressions=$(abs_top_srcdir)/src/valgrind-suppressions.revisit --num-callers=48 --leak-check=full --fullpath-after=" \
+	make -C $(abs_top_builddir) check
+else
+	@echo "*** Valgrind is required for check-valgrind-suppress ***"; exit 1;
+endif
+
 clean-cpp-coverage:
 	@if [ $(USE_LCOV) = yes ] ; then \
 		$(LCOV) --directory . --zerocounters; \
@@ -405,3 +425,5 @@ EXTRA_DIST += ext/coroutine/coroutine.h
 
 pkgconfigdir = $(libdir)/pkgconfig
 pkgconfig_DATA = dns++.pc
+
+CLEANFILES = $(abs_top_builddir)/logger_lockfile

+ 4 - 7
README

@@ -2,17 +2,14 @@
 This is the source for the development version of BIND 10.
 
 BIND is the popular implementation of a DNS server, developer
-interfaces, and DNS tools. BIND 10 is a rewrite of BIND 9. BIND 10
-is written in C++ and Python and provides a modular environment
-for serving, maintaining, and developing DNS.
+interfaces, and DNS tools. BIND 10 is a rewrite of BIND 9 and ISC
+DHCP. BIND 10 is written in C++ and Python and provides a modular
+environment for serving, maintaining, and developing DNS and DHCP.
 
 BIND10-devel is new development leading up to the production
 BIND 10 release. It contains prototype code and experimental
 interfaces. Nevertheless it is ready to use now for testing the
-new BIND 10 infrastructure ideas. The Year 3 goals of the five
-year plan are described here:
-
-        http://bind10.isc.org/wiki/Year3Goals
+new BIND 10 infrastructure ideas.
 
 This release includes the bind10 master process, b10-msgq message
 bus, b10-auth authoritative DNS server (with SQLite3 and in-memory

+ 11 - 6
compatcheck/Makefile.am

@@ -1,12 +1,17 @@
-# We're going to abuse install-data-local for a pre-install check.
-# This is to be considered a short term hack and is expected to be removed
-# in a near future version.
+# We're going to abuse install-data-local for a pre-install check.  This may
+# not be the cleanest way to do this type of job, but that's the least ugly
+# one we've found.
+#
+# Note also that if any test needs to examine some file that has possibly
+# been installed before (e.g., older DB or configuration file), it should be
+# referenced with the prefix of DESTDIR.  Otherwise
+# 'make DESTDIR=/somewhere install' may not work.
 install-data-local:
-	if test -e $(localstatedir)/$(PACKAGE)/zone.sqlite3; then \
+	if test -e $(DESTDIR)$(localstatedir)/$(PACKAGE)/zone.sqlite3; then \
 		$(SHELL) $(top_builddir)/src/bin/dbutil/run_dbutil.sh --check \
-		$(localstatedir)/$(PACKAGE)/zone.sqlite3 || \
+		$(DESTDIR)$(localstatedir)/$(PACKAGE)/zone.sqlite3 || \
 		(echo "\nSQLite3 DB file schema version is old.  " \
 		"Please run: " \
 		"$(abs_top_builddir)/src/bin/dbutil/run_dbutil.sh --upgrade " \
-		"$(localstatedir)/$(PACKAGE)/zone.sqlite3";  exit 1) \
+		"$(DESTDIR)$(localstatedir)/$(PACKAGE)/zone.sqlite3"; exit 1) \
 	fi

+ 19 - 3
configure.ac

@@ -362,7 +362,7 @@ if test $werror_ok = 1; then
 		 PYTHON_CXXFLAGS="${PYTHON_CXXFLAGS} -Wno-unused-parameter"
 		 AC_SUBST(PYTHON_CXXFLAGS)
 		],
-		[AC_MSG_ERROR([Can't compile against Python.h])]
+		[AC_MSG_ERROR([Can't compile against Python.h.  If you're using MacOS X and have installed Python with Homebrew, see http://bind10.isc.org/wiki/SystemNotesMacOSX])]
                 )
                 ]
 	)
@@ -407,9 +407,9 @@ case $system in
       OS_TYPE="BSD"
       CPPFLAGS="$CPPFLAGS -DOS_BSD"
       ;;
-    Solaris)
+    SunOS)
       OS_TYPE="Solaris"
-      CPPFLAGS="$CPPFLAGS -DOS_SOLARIS"
+      CPPFLAGS="$CPPFLAGS -DOS_SUN"
       ;;
     *)
       OS_TYPE="Unknown"
@@ -982,6 +982,15 @@ AC_ARG_ENABLE(logger-checks, [AC_HELP_STRING([--enable-logger-checks],
 AM_CONDITIONAL(ENABLE_LOGGER_CHECKS, test x$enable_logger_checks != xno)
 AM_COND_IF([ENABLE_LOGGER_CHECKS], [AC_DEFINE([ENABLE_LOGGER_CHECKS], [1], [Check logger messages?])])
 
+# Check for valgrind
+AC_PATH_PROG(VALGRIND, valgrind, no)
+AM_CONDITIONAL(HAVE_VALGRIND, test "x$VALGRIND" != "xno")
+
+found_valgrind="not found"
+if test "x$VALGRIND" != "xno"; then
+   found_valgrind="found"
+fi
+
 AC_CONFIG_FILES([Makefile
                  doc/Makefile
                  doc/guide/Makefile
@@ -1067,6 +1076,8 @@ AC_CONFIG_FILES([Makefile
                  src/lib/python/isc/testutils/Makefile
                  src/lib/python/isc/bind10/Makefile
                  src/lib/python/isc/bind10/tests/Makefile
+                 src/lib/python/isc/ddns/Makefile
+                 src/lib/python/isc/ddns/tests/Makefile
                  src/lib/python/isc/xfrin/Makefile
                  src/lib/python/isc/xfrin/tests/Makefile
                  src/lib/python/isc/server_common/Makefile
@@ -1120,6 +1131,7 @@ AC_CONFIG_FILES([Makefile
                  tests/tools/badpacket/Makefile
                  tests/tools/badpacket/tests/Makefile
                  tests/tools/perfdhcp/Makefile
+                 tests/tools/perfdhcp/tests/Makefile
                  dns++.pc
                ])
 AC_OUTPUT([doc/version.ent
@@ -1184,6 +1196,7 @@ AC_OUTPUT([doc/version.ent
            src/lib/log/tests/destination_test.sh
            src/lib/log/tests/init_logger_test.sh
            src/lib/log/tests/local_file_test.sh
+           src/lib/log/tests/logger_lock_test.sh
            src/lib/log/tests/severity_test.sh
            src/lib/log/tests/tempdir.h
            src/lib/util/python/mkpywrapper.py
@@ -1232,6 +1245,7 @@ AC_OUTPUT([doc/version.ent
            chmod +x src/lib/log/tests/destination_test.sh
            chmod +x src/lib/log/tests/init_logger_test.sh
            chmod +x src/lib/log/tests/local_file_test.sh
+           chmod +x src/lib/log/tests/logger_lock_test.sh
            chmod +x src/lib/log/tests/severity_test.sh
            chmod +x src/lib/util/python/mkpywrapper.py
            chmod +x src/lib/util/python/gen_wiredata.py
@@ -1287,8 +1301,10 @@ Features:
 
 Developer:
   Google Tests:  $gtest_path
+  Valgrind: $found_valgrind
   C++ Code Coverage: $USE_LCOV
   Python Code Coverage: $USE_PYCOVERAGE
+  Logger checks: $enable_logger_checks
   Generate Manuals:  $enable_man
 
 END

+ 1 - 1
doc/Doxyfile

@@ -579,7 +579,7 @@ INPUT                  = ../src/lib/exceptions ../src/lib/cc \
     ../src/lib/testutils ../src/lib/cache ../src/lib/server_common/ \
     ../src/bin/sockcreator/ ../src/lib/util/ ../src/lib/util/io/ \
     ../src/lib/resolve ../src/lib/acl ../src/bin/dhcp6 ../src/lib/dhcp \
-    ../src/bin/dhcp4 devel
+    ../src/bin/dhcp4 ../tests/tools/perfdhcp devel
 
 # This tag can be used to specify the character encoding of the source files
 # that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is

+ 4 - 3
doc/guide/Makefile.am

@@ -1,6 +1,7 @@
-EXTRA_DIST = bind10-guide.css
-EXTRA_DIST += bind10-guide.xml bind10-guide.html bind10-guide.txt
-EXTRA_DIST += bind10-messages.xml bind10-messages.html
+dist_doc_DATA = bind10-guide.txt
+dist_html_DATA = bind10-guide.css bind10-guide.html bind10-messages.html
+
+EXTRA_DIST = bind10-guide.xml bind10-messages.xml
 
 # This is not a "man" manual, but reuse this for now for docbook.
 if ENABLE_MAN

File diff suppressed because it is too large
+ 380 - 80
doc/guide/bind10-guide.html


File diff suppressed because it is too large
+ 566 - 272
doc/guide/bind10-guide.txt


+ 422 - 20
doc/guide/bind10-guide.xml

@@ -87,9 +87,10 @@
     <section>
       <title>Supported Platforms</title>
       <para>
-  BIND 10 builds have been tested on Debian GNU/Linux 5 and unstable,
-  Ubuntu 9.10, NetBSD 5, Solaris 10, FreeBSD 7 and 8, CentOS
-  Linux 5.3, and MacOS 10.6.
+  BIND 10 builds have been tested on (in no particular order)
+  Debian GNU/Linux 5 and unstable, Ubuntu 9.10, NetBSD 5,
+  Solaris 10 and 11, FreeBSD 7 and 8, CentOS Linux 5.3,
+  MacOS 10.6 and 10.7, and OpenBSD 5.1.
 
   It has been tested on Sparc, i386, and amd64 hardware
   platforms.
@@ -127,11 +128,13 @@
       </para>
 
       <para>
-        The <command>b10-xfrin</command>, <command>b10-xfrout</command>,
-        and <command>b10-zonemgr</command> components require the
-        libpython3 library and the Python _sqlite3.so module
-        (which is included with Python).
-        The Python module needs to be built for the corresponding Python 3.
+        The <command>b10-ddns</command>, <command>b10-xfrin</command>,
+        <command>b10-xfrout</command>, and <command>b10-zonemgr</command>
+        components require the libpython3 library and the Python
+        _sqlite3.so module (which is included with Python).
+        The <command>b10-stats-httpd</command> component uses the
+        Python pyexpat.so module.
+        The Python modules need to be built for the corresponding Python 3.
       </para>
 <!-- TODO: this will change ... -->
 
@@ -196,6 +199,16 @@
 
           <listitem>
             <simpara>
+              <command>b10-ddns</command> &mdash;
+              Dynamic DNS update service.
+              This process is used to handle incoming DNS update
+              requests to allow granted clients to update zones
+	      for which BIND 10 is serving as a primary server.
+            </simpara>
+          </listitem>
+
+          <listitem>
+            <simpara>
               <command>b10-msgq</command> &mdash;
               Message bus daemon.
               This process coordinates communication between all of the other
@@ -876,7 +889,7 @@ as a dependency earlier -->
         message bus. The special components already know their
         address, but the usual ones don't. The address is by
         convention the thing after <emphasis>b10-</emphasis>, with
-        the first letter capital (eg. <command>b10-stats</command>
+        the first letter capitalized (eg. <command>b10-stats</command>
         would have <quote>Stats</quote> as its address).
 <!-- TODO: this should be simplified so we don't even have to document it -->
       </para>
@@ -1321,9 +1334,10 @@ This may be a temporary setting until then.
       <varname>class</varname> to optionally select the class
       (it defaults to <quote>IN</quote>);
       and
-      <varname>zones</varname> to define the
-      <varname>file</varname> path name and the
-      <varname>origin</varname> (default domain).
+      <varname>zones</varname> to define
+      the <varname>file</varname> path name,
+      the <varname>filetype</varname> (e.g., <varname>sqlite3</varname>),
+      and the <varname>origin</varname> (default domain).
 
       By default, this is empty.
 
@@ -1333,7 +1347,8 @@ This may be a temporary setting until then.
         Only the IN class is supported at this time.
         By default, the memory data source is disabled.
         Also, currently the zone file must be canonical such as
-        generated by <command>named-compilezone -D</command>.
+        generated by <command>named-compilezone -D</command>, or
+        must be an SQLite3 database.
       </simpara></note>
 
               </simpara>
@@ -1350,6 +1365,24 @@ This may be a temporary setting until then.
       and <varname>port</varname> number.
       By default, <command>b10-auth</command> listens on port 53
       on the IPv6 (::) and IPv4 (0.0.0.0) wildcard addresses.
+      <note>
+        <simpara>
+          The default configuration is currently not appropriate for a multi-homed host.
+          In case you have multiple public IP addresses, it is possible the
+          query UDP packet comes through one interface and the answer goes out
+          through another. The answer will probably be dropped by the client, as it
+          has a different source address than the one it sent the query to. The
+          client would fallback on TCP after several attempts, which works
+          well in this situation, but is clearly not ideal.
+        </simpara>
+        <simpara>
+          There are plans to solve the problem such that the server handles
+          it by itself. But until it is actually implemented, it is recommended to
+          alter the configuration &mdash; remove the wildcard addresses and list all
+          addresses explicitly. Then the server will answer on the same
+          interface the request came on, preserving the correct address.
+        </simpara>
+      </note>
               </simpara>
             </listitem>
           </varlistentry>
@@ -1478,6 +1511,39 @@ This may be a temporary setting until then.
 	  after it is loaded.
 	</para>
 
+      </section>
+
+      <section id="in-memory-datasource-with-sqlite3-backend">
+	<title>In-memory Data Source With SQLite3 Backend</title>
+
+	<para>
+<!--	  How to configure it. -->
+	  The following commands to <command>bindctl</command>
+	  provide an example of configuring an in-memory data
+	  source containing the <quote>example.org</quote> zone
+	  with a SQLite3 backend file named <quote>example.org.sqlite3</quote>:
+
+<!--
+	  <screen>&gt; <userinput> config set Auth/datasources/ [{"type": "memory", "zones": [{"origin": "example.org", "file": "example.org.sqlite3", "filetype": "sqlite3"}]}]</userinput></screen>
+-->
+
+          <screen>&gt; <userinput>config add Auth/datasources</userinput>
+&gt; <userinput>config set Auth/datasources[1]/type "<option>memory</option>"</userinput>
+&gt; <userinput>config add Auth/datasources[1]/zones</userinput>
+&gt; <userinput>config set Auth/datasources[1]/zones[0]/origin "<option>example.org</option>"</userinput>
+&gt; <userinput>config set Auth/datasources[1]/zones[0]/file "<option>example.org.sqlite3</option>"</userinput>
+&gt; <userinput>config set Auth/datasources[1]/zones[0]/filetype "<option>sqlite3</option>"</userinput>
+&gt; <userinput>config commit</userinput></screen>
+
+	  The authoritative server will begin serving it immediately
+	  after it is loaded.
+	</para>
+
+      </section>
+
+      <section id="in-memory-datasource-loading">
+	<title>Reloading an In-memory Data Source</title>
+
 	<para>
 	  Use the <command>Auth loadzone</command> command in
 	  <command>bindctl</command> to reload a changed master
@@ -1496,6 +1562,10 @@ This may be a temporary setting until then.
 	</para>
 -->
 
+      </section>
+      <section id="in-memory-datasource-disabling">
+	<title>Disabling In-memory Data Sources</title>
+
         <para>
 	By default, the memory data source is disabled; it must be
 	configured explicitly.  To disable all the in-memory zones,
@@ -1628,12 +1698,6 @@ TODO
     </para>
 <!-- TODO: http://bind10.isc.org/ticket/1279 -->
 
-    <note><simpara>
-     In the current development release of BIND 10, incoming zone
-     transfers are only available for SQLite3-based data sources,
-     that is, they don't work for an in-memory data source.
-    </simpara></note>
-
     <section>
       <title>Configuration for Incoming Zone Transfers</title>
       <para>
@@ -1753,6 +1817,26 @@ what if a NOTIFY is sent?
       </para>
     </section>
 
+    <section>
+      <title>Incoming Transfers with In-memory Datasource</title>
+
+      <para>
+        In the case of an incoming zone transfer, the received zone is
+        first stored in the corresponding BIND 10 datasource. In
+        case the secondary zone is served by an in-memory datasource
+        with an SQLite3 backend, <command>b10-auth</command> is
+        automatically sent a <varname>loadzone</varname> command to
+        reload the corresponding zone into memory from the backend.
+      </para>
+
+      <para>
+	The administrator doesn't have to do anything for
+	<command>b10-auth</command> to serve the new version of the
+	zone, except for the configuration such as the one described in
+	<xref linkend="in-memory-datasource-with-sqlite3-backend" />.
+      </para>
+    </section>
+
 <!-- TODO: can that retransfer be used to identify a new zone? -->
 <!-- TODO: what if doesn't exist at that master IP? -->
 
@@ -1760,7 +1844,6 @@ what if a NOTIFY is sent?
 
   <chapter id="xfrout">
     <title>Outbound Zone Transfers</title>
-
     <para>
       The <command>b10-xfrout</command> process is started by
       <command>bind10</command>.
@@ -1836,6 +1919,325 @@ what is XfroutClient xfr_client??
 
   </chapter>
 
+  <chapter id="ddns">
+    <title>Dynamic DNS Update</title>
+
+    <para>
+      BIND 10 supports the server side of the Dynamic DNS Update
+      (DDNS) protocol as defined in RFC 2136.
+      This service is provided by the <command>b10-ddns</command>
+      component, which is started by the <command>bind10</command>
+      process if configured so.
+    </para>
+
+    <para>
+      When the <command>b10-auth</command> authoritative DNS server
+      receives an UPDATE request, it internally forwards the request
+      to <command>b10-ddns</command>, which handles the rest of
+      request processing.
+      When the processing is completed <command>b10-ddns</command>
+      will send a response to the client with the RCODE set to the
+      value as specified in RFC 2136 (NOERROR for successful update,
+      REFUSED if rejected due to ACL check, etc).
+      If the zone has been changed as a result, it will internally
+      notify <command>b10-xfrout</command> so that other secondary
+      servers will be notified via the DNS notify protocol.
+      In addition, if <command>b10-auth</command> serves the updated
+      zone from its in-memory cache (as described in
+      <xref linkend="in-memory-datasource-with-sqlite3-backend" />),
+      <command>b10-ddns</command> will also
+      notify <command>b10-auth</command> so that <command>b10-auth</command>
+      will re-cache the updated zone content.
+    </para>
+
+    <para>
+      The <command>b10-ddns</command> component supports requests over
+      both UDP and TCP, and both IPv6 and IPv4; for TCP requests,
+      however, it terminates the TCP connection immediately after
+      each single request has been processed.  Clients cannot reuse the
+      same TCP connection for multiple requests. (This is a current
+      implementation limitation of <command>b10-ddns</command>.
+      While RFC 2136 doesn't specify anything about such reuse of TCP
+      connection, there is no reason for disallowing it as RFC 1035
+      generally allows multiple requests sent over a single TCP
+      connection.  BIND 9 supports such reuse.)
+    </para>
+
+    <para>
+      As of this writing <command>b10-ddns</command> does not support
+      update forwarding for secondary zones.
+      If it receives an update request for a secondary zone, it will
+      immediately return a response with an RCODE of NOTIMP.
+      <note><simpara>
+	  For feature completeness update forwarding should be
+	  eventually supported.  But right now it's considered a lower
+	  priority task and there is no specific plan of implementing
+	  this feature.
+<!-- See Trac #2063 -->
+      </simpara></note>
+    </para>
+
+    <section>
+      <title>Enabling Dynamic Update</title>
+      <para>
+        First off, it must be made sure that a few components on which
+        <command>b10-ddns</command> depends are configured to run,
+        which are <command>b10-auth</command>
+        and <command>b10-zonemgr</command>.
+        In addition, <command>b10-xfrout</command> should also be
+        configured to run; otherwise the notification after an update
+        (see above) will fail with a timeout, suspending the DDNS
+        service while <command>b10-ddns</command> waits for the
+        response (see the description of the <ulink
+        url="bind10-messages.html#DDNS_UPDATE_NOTIFY_FAIL">DDNS_UPDATE_NOTIFY_FAIL</ulink>
+        log message for further details).
+        If BIND 10 is already configured to provide authoritative DNS
+        service they should normally be configured to run already.
+      </para>
+
+      <para>
+        Second, for the obvious reason dynamic update requires that the
+        underlying data source storing the zone data be writable.
+        In the current implementation this means the zone must be stored
+        in an SQLite3-based data source.
+        Also, right now, the <command>b10-ddns</command> component
+        configures itself with the data source referring to the
+        <quote>database_file</quote> configuration parameter of
+        <command>b10-auth</command>.
+        So this information must be configured correctly before starting
+        <command>b10-ddns</command>.
+
+        <note><simpara>
+            The way to configure data sources is now being revised.
+            Configuration on the data source for DDNS will be very
+            likely to be changed in a backward incompatible manner in
+            a near future version.
+        </simpara></note>
+      </para>
+
+      <para>
+	In general, if something goes wrong regarding the dependency
+	described above, <command>b10-ddns</command> will log the
+	related event at the warning or error level.
+	It's advisable to check the log message when you first enable
+	DDNS or if it doesn't work as you expect to see if there's any
+	warning or error log message.
+      </para>
+
+      <para>
+        Next, to enable the DDNS service, <command>b10-ddns</command>
+        needs to be explicitly configured to run.
+        It can be done by using the <command>bindctl</command>
+        utility.  For example:
+      <screen>
+&gt; <userinput>config add Boss/components b10-ddns</userinput>
+&gt; <userinput>config set Boss/components/b10-ddns/address DDNS</userinput>
+&gt; <userinput>config set Boss/components/b10-ddns/kind dispensable</userinput>
+&gt; <userinput>config commit</userinput>
+</screen>
+      <note><simpara>
+	  In theory "kind" could be omitted because "dispensable" is its
+	  default.  But there's some peculiar behavior (which should
+	  be a bug and should be fixed eventually; see Trac ticket
+	  #2064) with bindctl and you'll still need to specify that explicitly.
+	  Likewise, "address" may look unnecessary because
+	  <command>b10-ddns</command> would start and work without
+	  specifying it.  But for it to shutdown gracefully this
+	  parameter should also be specified.
+      </simpara></note>
+      </para>
+    </section>
+
+    <section>
+      <title>Access Control</title>
+      <para>
+        By default <command>b10-ddns</command> rejects any update
+        requests from any clients by returning a response with an RCODE
+        of REFUSED.
+        To allow updates to take effect, an access control rule
+        (called update ACL) with a policy allowing updates must explicitly be
+        configured.
+        Update ACL must be configured per zone basis in the
+        <quote>zones</quote> configuration parameter of
+        <command>b10-ddns</command>.
+        This is a list of per-zone configurations regarding DDNS.
+        Each list element consists of the following parameters:
+        <variablelist>
+          <varlistentry>
+            <term>origin</term>
+            <listitem>
+              <simpara>The zone's origin name</simpara>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>class</term>
+            <listitem>
+              <simpara>The RR class of the zone
+                (normally <quote>IN</quote>, and in that case
+		can be omitted in configuration)</simpara>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>update_acl</term>
+            <listitem>
+              <simpara>List of access control rules (ACL) for the zone</simpara>
+            </listitem>
+          </varlistentry>
+        </variablelist>
+        The syntax of the ACL is the same as ACLs for other
+        components.
+        Specific examples are given below.
+      </para>
+
+      <para>
+        In general, an update ACL rule that allows an update request
+        should be configured with a TSIG key.
+        This is an example update ACL that allows updates to the zone
+        named <quote>example.org</quote> of RR class <quote>IN</quote>
+        from clients that send requests signed with a TSIG whose
+        key name is "key.example.org" (and refuses all others):
+      <screen>
+&gt; <userinput>config add DDNS/zones</userinput>
+&gt; <userinput>config set DDNS/zones[0]/origin example.org</userinput>
+&gt; <userinput>config set DDNS/zones[0]/class IN</userinput>
+(Note: "class" can be omitted)
+&gt; <userinput>config add DDNS/zones[0]/update_acl {"action": "ACCEPT", "key": "key.example.org"}</userinput>
+&gt; <userinput>config commit</userinput>
+</screen>
+      The TSIG key must be configured system wide
+      (see <xref linkend="xfrout"/>.)
+      </para>
+
+      <para>
+        Multiple rules can be specified in the ACL, and an ACL rule
+        can consist of multiple constraints, such as a combination of
+        IP address and TSIG.
+        The following configuration sequence will add a new rule to
+        the ACL created in the above example.  This additional rule
+	allows update requests sent from a client
+        using TSIG key name of "key.example" (different from the
+        key used in the previous example) and has an IPv6 address of ::1.
+      <screen>
+&gt; <userinput>config add DDNS/zones[0]/update_acl {"action": "ACCEPT", "from": "::1", "key": "key.example"}</userinput>
+&gt; <userinput>config show DDNS/zones[0]/update_acl</userinput>
+DDNS/zones[0]/update_acl[0]     {"action": "ACCEPT", "key": "key.example.org"} any (modified)
+DDNS/zones[0]/update_acl[1]     {"action": "ACCEPT", "from": "::1", "key": "key.example"} any (modified)
+&gt; <userinput>config commit</userinput>
+</screen>
+      (Note the "add" in the first line.  Before this sequence, we
+      have had only entry in zones[0]/update_acl.  The "add" command
+      with a value (rule) adds a new entry and sets it to the given rule.
+      Due to a limitation of the current implementation, it doesn't
+      work if you first try to just add a new entry and then set it to
+      a given rule).
+      </para>
+
+      <note><simpara>
+          The <command>b10-ddns</command> component accepts an ACL
+          rule that just allows updates from a specific IP address
+          (i.e., without requiring TSIG), but this is highly
+          discouraged (remember that requests can be made over UDP and
+          spoofing the source address of a UDP packet is often pretty
+          easy).
+          Unless you know what you are doing and that you can accept
+          its consequence, any update ACL rule that allows updates
+          should have a TSIG key in its constraints.
+      </simpara></note>
+
+      <para>
+	The ACL rules will be checked in the listed order, and the
+	first matching one will apply.
+	If none of the rules matches, the default rule will apply,
+	which is rejecting any requests in the case of
+	<command>b10-ddns</command>.
+      </para>
+
+      <para>
+	Other actions than "ACCEPT", namely "REJECT" and "DROP", can be
+	used, too.
+	See <xref linkend="resolverserver"/> about their effects.
+      </para>
+
+      <para>
+        Currently update ACL can only control updates per zone basis;
+        it's not possible to specify access control with higher
+        granularity such as for particular domain names or specific
+        types of RRs.
+<!-- See Trac ticket #2065 -->
+      </para>
+
+      <note><simpara>
+        Contrary to what RFC 2136 (literally) specifies,
+        <command>b10-ddns</command> checks the update ACL before
+        checking the prerequisites of the update request.
+        This is a deliberate implementation decision.
+        This counter intuitive specification has been repeatedly
+        discussed among implementers and in the IETF, and it is now
+        widely agreed that it does not make sense to strictly follow
+        that part of RFC.
+        One known specific bad result of following the RFC is that it
+        could leak information about which name or record exists or does not
+        exist in the zone as a result of prerequisite checks even if a
+        zone is somehow configured to reject normal queries from
+        arbitrary clients.
+        There have been other troubles that could have been avoided if
+        the ACL could be checked before the prerequisite check.
+      </simpara></note>
+    </section>
+
+    <section>
+      <title>Miscellaneous Operational Issues</title>
+      <para>
+        Unlike BIND 9, BIND 10 currently does not support automatic
+        resigning of DNSSEC-signed zone when it's updated via DDNS.
+        It could be possible to resign the updated zone afterwards
+        or make sure the update request also updates related DNSSEC
+        records, but that will be pretty error-prone operation.
+        In general, it's not advisable to allow DDNS for a signed zone
+        at this moment.
+      </para>
+
+      <para>
+        Also unlike BIND 9, it's currently not possible
+        to <quote>freeze</quote> a zone temporarily in order to
+        suspend DDNS while you manually update the zone.
+        If you need to make manual updates to a dynamic zone,
+        you'll need to temporarily reject any updates to the zone via
+        the update ACLs.
+      </para>
+
+      <para>
+        Dynamic updates are only applicable to primary zones.
+        In order to avoid updating secondary zones via DDNS requests,
+        <command>b10-ddns</command> refers to the
+        <quote>secondary_zones</quote> configuration of
+        <command>b10-zonemgr</command>.  Zones listed in
+        <quote>secondary_zones</quote> will never be updated via DDNS
+        regardless of the update ACL configuration;
+	<command>b10-ddns</command> will return a response with an
+	RCODE of NOTAUTH as specified in RFC 2136.
+        If you have a "conceptual" secondary zone whose content is a
+        copy of some external source but is not updated via the
+        standard zone transfers and therefore not listed in
+        <quote>secondary_zones</quote>, be careful not to allow DDNS
+        for the zone; it would be quite likely to lead to inconsistent
+        state between different servers.
+        Normally this should not be a problem because the default
+        update ACL rejects any update requests, but you may want to
+        take an extra care about the configuration if you have such
+        type of secondary zones.
+      </para>
+      <para>
+        The difference of two versions of a zone, before and after a
+        DDNS transaction, is automatically recorded in the underlying
+        data source, and can be retrieved in the form of outbound
+        IXFR.
+        This is done automatically; it does not require specific
+        configuration to make this possible.
+      </para>
+    </section>
+  </chapter>
+
   <chapter id="resolverserver">
     <title>Recursive Name Server</title>
 

File diff suppressed because it is too large
+ 585 - 32
doc/guide/bind10-messages.html


+ 765 - 31
doc/guide/bind10-messages.xml

@@ -303,6 +303,24 @@ discovered that the memory data source is enabled for the given class.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="AUTH_MESSAGE_FORWARD_ERROR">
+<term>AUTH_MESSAGE_FORWARD_ERROR failed to forward %1 request from %2: %3</term>
+<listitem><para>
+The authoritative server tried to forward some type DNS request
+message to a separate process (e.g., forwarding dynamic update
+requests to b10-ddns) to handle it, but it failed.  The authoritative
+server returns SERVFAIL to the client on behalf of the separate
+process.  The error could be configuration mismatch between b10-auth
+and the recipient component, or it may be because the requests are
+coming too fast and the receipient process cannot keep up with the
+rate, or some system level failure.  In either case this means the
+BIND 10 system is not working as expected, so the administrator should
+look into the cause and address the issue.  The log message includes
+the client's address (and port), and the error message sent from the
+lower layer that detects the failure.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="AUTH_NOTIFY_QUESTIONS">
 <term>AUTH_NOTIFY_QUESTIONS invalid number of questions (%1) in incoming NOTIFY</term>
 <listitem><para>
@@ -882,6 +900,15 @@ The boss module is sending a SIGTERM signal to the given process.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="BIND10_SETGID">
+<term>BIND10_SETGID setting GID to %1</term>
+<listitem><para>
+The boss switches the process group ID to the given value.  This happens
+when BIND 10 starts with the -u option, and the group ID will be set to
+that of the specified user.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="BIND10_SETUID">
 <term>BIND10_SETUID setting UID to %1</term>
 <listitem><para>
@@ -1399,7 +1426,7 @@ Debug message. The RRset is updating its data with this given RRset.
 </varlistentry>
 
 <varlistentry id="CC_ASYNC_READ_FAILED">
-<term>CC_ASYNC_READ_FAILED asynchronous read failed</term>
+<term>CC_ASYNC_READ_FAILED asynchronous read failed (error code = %1)</term>
 <listitem><para>
 This marks a low level error, we tried to read data from the message queue
 daemon asynchronously, but the ASIO library returned an error.
@@ -1588,6 +1615,16 @@ are now applied, and no action from the administrator is necessary.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="CFGMGR_BACKED_UP_CONFIG_FILE">
+<term>CFGMGR_BACKED_UP_CONFIG_FILE Config file %1 was removed; a backup was made at %2</term>
+<listitem><para>
+BIND 10 has been started with the command to clear the configuration
+file.  The existing file has been backed up (moved) to the given file
+name. A new configuration file will be created in the original location
+when necessary.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="CFGMGR_BAD_UPDATE_RESPONSE_FROM_MODULE">
 <term>CFGMGR_BAD_UPDATE_RESPONSE_FROM_MODULE Unable to parse response from module %1: %2</term>
 <listitem><para>
@@ -1607,6 +1644,14 @@ system. The most likely cause is that msgq is not running.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="CFGMGR_CONFIG_FILE">
+<term>CFGMGR_CONFIG_FILE Configuration manager starting with configuration file: %1</term>
+<listitem><para>
+The configuration manager is starting, reading and saving the configuration
+settings to the shown file.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="CFGMGR_DATA_READ_ERROR">
 <term>CFGMGR_DATA_READ_ERROR error reading configuration database from disk: %1</term>
 <listitem><para>
@@ -1639,15 +1684,6 @@ configuration is not stored.
 </para></listitem>
 </varlistentry>
 
-<varlistentry id="CFGMGR_RENAMED_CONFIG_FILE">
-<term>CFGMGR_RENAMED_CONFIG_FILE renamed configuration file %1 to %2, will create new %1</term>
-<listitem><para>
-BIND 10 has been started with the command to clear the configuration file.
-The existing file is backed up to the given file name, so that data is not
-immediately lost if this was done by accident.
-</para></listitem>
-</varlistentry>
-
 <varlistentry id="CFGMGR_STOPPED_BY_KEYBOARD">
 <term>CFGMGR_STOPPED_BY_KEYBOARD keyboard interrupt, shutting down</term>
 <listitem><para>
@@ -2057,6 +2093,58 @@ in the answer as a result.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="DATASRC_DATABASE_FINDNSEC3">
+<term>DATASRC_DATABASE_FINDNSEC3 Looking for NSEC3 for %1 in %2 mode</term>
+<listitem><para>
+Debug information. A search in an database data source for NSEC3 that
+matches or covers the given name is being started.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="DATASRC_DATABASE_FINDNSEC3_COVER">
+<term>DATASRC_DATABASE_FINDNSEC3_COVER found a covering NSEC3 for %1 at label count %2: %3</term>
+<listitem><para>
+Debug information. An NSEC3 that covers the given name is found and
+being returned.  The found NSEC3 RRset is also displayed. When the shown label
+count is smaller than that of the given name, the matching NSEC3 is for a
+superdomain of the given name (see DATASRC_DATABSE_FINDNSEC3_TRYHASH).  The
+found NSEC3 RRset is also displayed.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="DATASRC_DATABASE_FINDNSEC3_MATCH">
+<term>DATASRC_DATABASE_FINDNSEC3_MATCH found a matching NSEC3 for %1 at label count %2: %3</term>
+<listitem><para>
+Debug information. An NSEC3 that matches (a possibly superdomain of)
+the given name is found and being returned.  When the shown label
+count is smaller than that of the given name, the matching NSEC3 is
+for a superdomain of the given name (see DATASRC_DATABSE_FINDNSEC3_TRYHASH).
+The found NSEC3 RRset is also displayed.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="DATASRC_DATABASE_FINDNSEC3_TRYHASH">
+<term>DATASRC_DATABASE_FINDNSEC3_TRYHASH looking for NSEC3 for %1 at label count %2 (hash %3)</term>
+<listitem><para>
+Debug information. In an attempt of finding an NSEC3 for the give name,
+(a possibly superdomain of) the name is hashed and searched for in the
+NSEC3 name space.  When the shown label count is smaller than that of the
+shown name, the search tries the superdomain name that share the shown
+(higher) label count of the shown name (e.g., for
+www.example.com. with shown label count of 3, example.com. is being
+tried, as "." is 1 label long).
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="DATASRC_DATABASE_FINDNSEC3_TRYHASH_PREV">
+<term>DATASRC_DATABASE_FINDNSEC3_TRYHASH_PREV looking for previous NSEC3 for %1 at label count %2 (hash %3)</term>
+<listitem><para>
+Debug information. An exact match on hash (see
+DATASRC_DATABASE_FINDNSEC3_TRYHASH) was unsuccessful. We get the previous hash
+to that one instead.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="DATASRC_DATABASE_FIND_RECORDS">
 <term>DATASRC_DATABASE_FIND_RECORDS looking in datasource %1 for record %2/%3/%4</term>
 <listitem><para>
@@ -2155,7 +2243,7 @@ has been requested and returned.
 </varlistentry>
 
 <varlistentry id="DATASRC_DATABASE_FOUND_RRSET">
-<term>DATASRC_DATABASE_FOUND_RRSET search in datasource %1 resulted in RRset %5</term>
+<term>DATASRC_DATABASE_FOUND_RRSET search in datasource %1 resulted in RRset %2</term>
 <listitem><para>
 The data returned by the database backend contained data for the given domain
 name, and it either matches the type or has a relevant type. The RRset that is
@@ -2189,10 +2277,12 @@ The name and RRtype of the RRset is indicated in the message.
 <varlistentry id="DATASRC_DATABASE_ITERATE_TTL_MISMATCH">
 <term>DATASRC_DATABASE_ITERATE_TTL_MISMATCH TTL values differ for RRs of %1/%2/%3, setting to %4</term>
 <listitem><para>
-While iterating through the zone, the time to live for RRs of the given RRset
-were found to be different. This isn't allowed on the wire and is considered
-an error, so we set it to the lowest value we found (but we don't modify the
-database). The data in database should be checked and fixed.
+While iterating through the zone, the time to live for RRs of the
+given RRset were found to be different. Since an RRset cannot have
+multiple TTLs, we set it to the lowest value we found (but we don't
+modify the database). This is what the client would do when such RRs
+were given in a DNS response according to RFC2181. The data in
+database should be checked and fixed.
 </para></listitem>
 </varlistentry>
 
@@ -2352,7 +2442,7 @@ namespace but has no RRs assopciated with it). This will produce NXRRSET.
 </varlistentry>
 
 <varlistentry id="DATASRC_DATABASE_WILDCARD_MATCH">
-<term>DATASRC_DATABASE_WILDCARD_MATCH search in datasource %1 resulted in wildcard match at %5 with RRset %6</term>
+<term>DATASRC_DATABASE_WILDCARD_MATCH search in datasource %1 resulted in wildcard match at %2 with RRset %3</term>
 <listitem><para>
 The database doesn't contain directly matching name.  When searching
 for a wildcard match, a wildcard record matching the name and type of
@@ -3096,6 +3186,21 @@ Debug information. The SQLite data source is closing the database file.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="DATASRC_SQLITE_COMPATIBLE_VERSION">
+<term>DATASRC_SQLITE_COMPATIBLE_VERSION database schema V%1.%2 not up to date (expecting V%3.%4) but is compatible</term>
+<listitem><para>
+The version of the SQLite3 database schema used to hold the zone data
+is not the latest one - the current version of BIND 10 was written
+with a later schema version in mind.  However, the database is
+compatible with the current version of BIND 10, and BIND 10 will run
+without any problems.
+</para><para>
+Consult the release notes for your version of BIND 10.  Depending on
+the changes made to the database schema, it is possible that improved
+performance could result if the database were upgraded.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="DATASRC_SQLITE_CONNCLOSE">
 <term>DATASRC_SQLITE_CONNCLOSE Closing sqlite database</term>
 <listitem><para>
@@ -3235,6 +3340,18 @@ But it doesn't contain that zone.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="DATASRC_SQLITE_INCOMPATIBLE_VERSION">
+<term>DATASRC_SQLITE_INCOMPATIBLE_VERSION database schema V%1.%2 incompatible with version (V%3.%4) expected</term>
+<listitem><para>
+The version of the SQLite3 database schema used to hold the zone data
+is incompatible with the version expected by BIND 10.  As a result,
+BIND 10 is unable to run using the database file as the data source.
+</para><para>
+The database should be updated using the means described in the BIND
+10 documentation.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="DATASRC_SQLITE_NEWCONN">
 <term>DATASRC_SQLITE_NEWCONN SQLite3Database is being initialized</term>
 <listitem><para>
@@ -3517,6 +3634,16 @@ is logged.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="DDNS_AUTH_DBFILE_UPDATE">
+<term>DDNS_AUTH_DBFILE_UPDATE updated auth DB file to %1</term>
+<listitem><para>
+b10-ddns was notified of updates to the SQLite3 DB file that b10-auth
+uses for the underlying data source and on which b10-ddns needs to
+make updates.  b10-ddns then updated its internal setup so further
+updates would be made on the new DB.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="DDNS_CC_SESSION_ERROR">
 <term>DDNS_CC_SESSION_ERROR error reading from cc channel: %1</term>
 <listitem><para>
@@ -3542,6 +3669,18 @@ startup time.  Details of the error are included in the log message.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="DDNS_CONFIG_HANDLER_ERROR">
+<term>DDNS_CONFIG_HANDLER_ERROR failed to update ddns configuration: %1</term>
+<listitem><para>
+An update to b10-ddns configuration was delivered but an error was
+found while applying them.  None of the delivered updates were applied
+to the running b10-ddns system, and the server will keep running with
+the existing configuration.  If this happened in the initial
+configuration setup, the server will be running with the default
+configurations.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="DDNS_DROP_CONN">
 <term>DDNS_DROP_CONN dropping connection on file descriptor %1 because of error %2</term>
 <listitem><para>
@@ -3553,6 +3692,33 @@ confused and sent bad data.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="DDNS_GET_REMOTE_CONFIG_FAIL">
+<term>DDNS_GET_REMOTE_CONFIG_FAIL failed to get %1 module configuration %2 times: %3</term>
+<listitem><para>
+b10-ddns tried to get configuration of some remote modules for its
+operation, but it failed.  The most likely cause of this is that the
+remote module has not fully started up and b10-ddns couldn't get the
+configuration in a timely fashion.  b10-ddns attempts to retry it a
+few times, imposing a short delay, hoping it eventually succeeds if
+it's just a timing issue.  The number of total failed attempts is also
+logged.  If it reaches an internal threshold b10-ddns considers it a
+fatal error and terminates.  Even in that case, if b10-ddns is
+configured as a "dispensable" component (which is the default), the
+parent bind10 process will restart it, and there will be another
+chance of getting the remote configuration successfully.  These are
+not the optimal behavior, but it's believed to be sufficient in
+practice (there would normally be no failure in the first place).  If
+it really causes an operational trouble other than having a few of
+these log messages, please submit a bug report; there can be several
+ways to make it more sophisticated.  Another, less likely reason for
+having this error is because the remote modules are not actually
+configured to run.  If that's the case fixing the configuration should
+solve the problem - either by making sure the remote module will run
+or by not running b10-ddns (without these remote modules b10-ddns is
+not functional, so there's no point in running it in this case).
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="DDNS_MODULECC_SESSION_ERROR">
 <term>DDNS_MODULECC_SESSION_ERROR error encountered by configuration/command module: %1</term>
 <listitem><para>
@@ -3574,6 +3740,15 @@ coming from a b10-auth process.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="DDNS_RECEIVED_AUTH_UPDATE">
+<term>DDNS_RECEIVED_AUTH_UPDATE received configuration updates from auth server</term>
+<listitem><para>
+b10-ddns is notified of updates to b10-auth configuration
+(including a report of the initial configuration) that b10-ddns might
+be interested in.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="DDNS_RECEIVED_SHUTDOWN_COMMAND">
 <term>DDNS_RECEIVED_SHUTDOWN_COMMAND shutdown command received</term>
 <listitem><para>
@@ -3582,11 +3757,105 @@ and will now shut down.
 </para></listitem>
 </varlistentry>
 
-<varlistentry id="DDNS_RUNNING">
-<term>DDNS_RUNNING ddns server is running and listening for updates</term>
+<varlistentry id="DDNS_RECEIVED_ZONEMGR_UPDATE">
+<term>DDNS_RECEIVED_ZONEMGR_UPDATE received configuration updates from zonemgr</term>
 <listitem><para>
-The ddns process has successfully started and is now ready to receive commands
-and updates.
+b10-ddns is notified of updates to b10-zonemgr's configuration
+(including a report of the initial configuration).  It may possibly
+contain changes to the secondary zones, in which case b10-ddns will
+update its internal copy of that configuration.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="DDNS_REQUEST_PARSE_FAIL">
+<term>DDNS_REQUEST_PARSE_FAIL failed to parse update request: %1</term>
+<listitem><para>
+b10-ddns received an update request via b10-auth, but the received
+data failed to pass minimum validation: it was either broken wire
+format data for a valid DNS message (e.g. it's shorter than the
+fixed-length header), or the opcode is not update, or TSIG is included
+in the request but it fails to validate.  Since b10-auth should have
+performed this level of checks, such an error shouldn't be detected at
+this stage and should rather be considered an internal bug.  This
+event is therefore logged at the error level, and the request is
+simply dropped.  Additional information of the error is also logged.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="DDNS_REQUEST_TCP_QUOTA">
+<term>DDNS_REQUEST_TCP_QUOTA reject TCP update client %1 (%2 running)</term>
+<listitem><para>
+b10-ddns received a new update request from a client over TCP, but
+the number of TCP clients being handled by the server already reached
+the configured quota, so the latest client was rejected by closing
+the connection.  The administrator may want to check the status of
+b10-ddns, and if this happens even if the server is not very busy,
+the quota may have to be increased.  Or, if it's more likely to be
+malicious or simply bogus clients that somehow keep the TCP connection
+open for a long period, maybe they should be rejected with an
+appropriate ACL configuration or some lower layer filtering.  The
+number of existing TCP clients are shown in the log, which should be
+identical to the current quota.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="DDNS_RESPONSE_SOCKET_ERROR">
+<term>DDNS_RESPONSE_SOCKET_ERROR failed to send update response to %1: %2</term>
+<listitem><para>
+Network I/O error happens in sending an update response.  The
+client's address that caused the error and error details are also
+logged.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="DDNS_RESPONSE_TCP_SOCKET_ERROR">
+<term>DDNS_RESPONSE_TCP_SOCKET_ERROR failed to complete sending update response to %1 over TCP</term>
+<listitem><para>
+b10-ddns had tried to send an update response over TCP, and it hadn't
+been completed at that time, and a followup attempt to complete the
+send operation failed due to some network I/O error.  While a network
+error can happen any time, this event is quite unexpected for two
+reasons.  First, since the size of a response to an update request
+should be generally small, it's unlikely that the initial attempt
+didn't fail but wasn't completed.  Second, since the first attempt
+succeeded and the TCP connection had been established in the first
+place, it's more likely for the subsequent attempt to succeed.  In any
+case, there may not be able to do anything to fix it at the server
+side, but the administrator may want to check the general reachability
+with the client address.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="DDNS_SECONDARY_ZONES_UPDATE">
+<term>DDNS_SECONDARY_ZONES_UPDATE updated secondary zone list (%1 zones are listed)</term>
+<listitem><para>
+b10-ddns has successfully updated the internal copy of secondary zones
+obtained from b10-zonemgr, based on a latest update to zonemgr's
+configuration.  The number of newly configured (unique) secondary
+zones is logged.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="DDNS_SECONDARY_ZONES_UPDATE_FAIL">
+<term>DDNS_SECONDARY_ZONES_UPDATE_FAIL failed to update secondary zone list: %1</term>
+<listitem><para>
+An error message.  b10-ddns was notified of updates to a list of
+secondary zones from b10-zonemgr and tried to update its own internal
+copy of the list, but it failed.  This can happen if the configuration
+contains an error, and b10-zonemgr should also reject that update.
+Unfortunately, in the current implementation there is no way to ensure
+that both zonemgr and ddns have consistent information when an update
+contains an error; further, as of this writing zonemgr has a bug that
+it could partially update the list of secondary zones if part of the
+list has an error (see Trac ticket #2038).  b10-ddns still keeps
+running with the previous configuration, but it's strongly advisable
+to check log messages from zonemgr, and if it indicates there can be
+inconsistent state, it's better to restart the entire BIND 10 system
+(just restarting b10-ddns wouldn't be enough, because zonemgr can have
+partially updated configuration due to bug #2038).  The log message
+contains an error description, but it's intentionally kept simple as
+it's primarily a matter of zonemgr.  To know the details of the error,
+log messages of zonemgr should be consulted.
 </para></listitem>
 </varlistentry>
 
@@ -3608,6 +3877,14 @@ be completed, after which the process will exit.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="DDNS_STARTED">
+<term>DDNS_STARTED ddns server is running and listening for updates</term>
+<listitem><para>
+The ddns process has successfully started and is now ready to receive commands
+and updates.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="DDNS_STOPPED">
 <term>DDNS_STOPPED ddns server has stopped</term>
 <listitem><para>
@@ -3633,6 +3910,362 @@ normal circumstances. The exception type and message are printed.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="DDNS_UPDATE_NOTIFY">
+<term>DDNS_UPDATE_NOTIFY notified %1 of updates to %2</term>
+<listitem><para>
+Debug message.  b10-ddns has made updates to a zone based on an update
+request and has successfully notified an external module of the updates.
+The notified module will use that information for updating its own
+state or any necessary protocol action such as zone reloading or sending
+notify messages to secondary servers.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="DDNS_UPDATE_NOTIFY_FAIL">
+<term>DDNS_UPDATE_NOTIFY_FAIL failed to notify %1 of updates to %2: %3</term>
+<listitem><para>
+b10-ddns has made updates to a zone based on an update request and
+tried to notify an external component of the updates, but the
+notification fails.  One possible cause of this is that the external
+component is not really running and it times out in waiting for the
+response, although it will be less likely to happen in practice
+because these components will normally be configured to run when the
+server provides the authoritative DNS service; ddns is rather optional
+among them.  If this happens, however, it will suspend b10-ddns for a
+few seconds during which it cannot handle new requests (some may be
+delayed, some may be dropped, depending on the volume of the incoming
+requests).  This is obviously bad, and if this error happens due to
+this reason, the administrator should make sure the component in
+question should be configured to run.  For a longer term, b10-ddns
+should be more robust about this case such as by making this
+notification asynchronously and/or detecting the existence of the
+external components to avoid hopeless notification in the first place.
+Severity of this error for the receiving components depends on the
+type of the component.  If it's b10-xfrout, this means DNS notify
+messages won't be sent to secondary servers of the zone.  It's
+suboptimal, but not necessarily critical as the secondary servers will
+try to check the zone's status periodically.  If it's b10-auth and the
+notification was needed to have it reload the corresponding zone, it's
+more serious because b10-auth won't be able to serve the new version
+of the zone unless some explicit recovery action is taken.  So the
+administrator needs to examine this message and takes an appropriate
+action.  In either case, this notification is generally expected to
+succeed; so the fact it fails itself means there's something wrong in
+the BIND 10 system, and it would be advisable to check other log
+messages.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_DATASRC_ERROR">
+<term>LIBDDNS_DATASRC_ERROR update client %1 failed due to data source error: %2</term>
+<listitem><para>
+An update attempt failed due to some error in the corresponding data
+source.  This is generally an unexpected event, but can still happen
+for various reasons such as DB lock contention or a failure of the
+backend DB server.  The cause of the error is also logged.  It's
+advisable to check the message, and, if necessary, take an appropriate
+action (e.g., restarting the DB server if it dies).  If this message
+is logged the data source isn't modified due to the
+corresponding update request.  When used by the b10-ddns, the server
+will return a response with an RCODE of SERVFAIL.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_PREREQ_FORMERR">
+<term>LIBDDNS_PREREQ_FORMERR update client %1 for zone %2: Format error in prerequisite (%3). Non-zero TTL.</term>
+<listitem><para>
+The prerequisite with the given name, class and type is not well-formed.
+The specific prerequisite is shown. In this case, it has a non-zero TTL value.
+A FORMERR error response is sent to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_PREREQ_FORMERR_ANY">
+<term>LIBDDNS_PREREQ_FORMERR_ANY update client %1 for zone %2: Format error in prerequisite (%3). Non-zero TTL or rdata found.</term>
+<listitem><para>
+The prerequisite with the given name, class and type is not well-formed.
+The specific prerequisite is shown. In this case, it either has a non-zero
+TTL value, or has rdata fields. A FORMERR error response is sent to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_PREREQ_FORMERR_CLASS">
+<term>LIBDDNS_PREREQ_FORMERR_CLASS update client %1 for zone %2: Format error in prerequisite (%3). Bad class.</term>
+<listitem><para>
+The prerequisite with the given name, class and type is not well-formed.
+The specific prerequisite is shown. In this case, the class of the
+prerequisite should either match the class of the zone in the Zone Section,
+or it should be ANY or NONE, and it is not. A FORMERR error response is sent
+to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_PREREQ_FORMERR_NONE">
+<term>LIBDDNS_PREREQ_FORMERR_NONE update client %1 for zone %2: Format error in prerequisite (%3). Non-zero TTL or rdata found.</term>
+<listitem><para>
+The prerequisite with the given name, class and type is not well-formed.
+The specific prerequisite is shown. In this case, it either has a non-zero
+TTL value, or has rdata fields. A FORMERR error response is sent to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_PREREQ_NAME_IN_USE_FAILED">
+<term>LIBDDNS_PREREQ_NAME_IN_USE_FAILED update client %1 for zone %2: 'Name is in use' prerequisite not satisfied (%3), rcode: %4</term>
+<listitem><para>
+A DNS UPDATE prerequisite was not satisfied. The specific prerequisite that
+was not satisfied is shown. The client is sent an error response with the
+given rcode.
+In this case, the specific prerequisite is 'Name is in use'. From RFC2136:
+Name is in use.  At least one RR with a specified NAME (in
+the zone and class specified by the Zone Section) must exist.
+Note that this prerequisite is NOT satisfied by empty
+nonterminals.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_PREREQ_NAME_NOT_IN_USE_FAILED">
+<term>LIBDDNS_PREREQ_NAME_NOT_IN_USE_FAILED update client %1 for zone %2: 'Name is not in use' (%3) prerequisite not satisfied, rcode: %4</term>
+<listitem><para>
+A DNS UPDATE prerequisite was not satisfied. The specific prerequisite that
+was not satisfied is shown. The client is sent an error response with the
+given rcode.
+In this case, the specific prerequisite is 'Name is not in use'.
+From RFC2136:
+Name is not in use.  No RR of any type is owned by a
+specified NAME.  Note that this prerequisite IS satisfied by
+empty nonterminals.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_PREREQ_NOTZONE">
+<term>LIBDDNS_PREREQ_NOTZONE update client %1 for zone %2: prerequisite not in zone (%3)</term>
+<listitem><para>
+A DDNS UPDATE prerequisite has a name that does not appear to be inside
+the zone specified in the Zone section of the UPDATE message.
+The specific prerequisite is shown. A NOTZONE error response is sent to
+the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_PREREQ_RRSET_DOES_NOT_EXIST_FAILED">
+<term>LIBDDNS_PREREQ_RRSET_DOES_NOT_EXIST_FAILED update client %1 for zone %2: 'RRset does not exist' (%3) prerequisite not satisfied, rcode: %4</term>
+<listitem><para>
+A DNS UPDATE prerequisite was not satisfied. The specific prerequisite that
+was not satisfied is shown. The client is sent an error response with the
+given rcode.
+In this case, the specific prerequisite is 'RRset does not exist'.
+From RFC2136:
+RRset does not exist.  No RRs with a specified NAME and TYPE
+(in the zone and class denoted by the Zone Section) can exist.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_PREREQ_RRSET_EXISTS_FAILED">
+<term>LIBDDNS_PREREQ_RRSET_EXISTS_FAILED update client %1 for zone %2: 'RRset exists (value independent)' (%3) prerequisite not satisfied, rcode: %4</term>
+<listitem><para>
+A DNS UPDATE prerequisite was not satisfied. The specific prerequisite that
+was not satisfied is shown. The client is sent an error response with the
+given rcode.
+In this case, the specific prerequisite is 'RRset exists (value independent)'.
+From RFC2136:
+RRset exists (value dependent).  A set of RRs with a
+specified NAME and TYPE exists and has the same members
+with the same RDATAs as the RRset specified here in this
+Section.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_PREREQ_RRSET_EXISTS_VAL_FAILED">
+<term>LIBDDNS_PREREQ_RRSET_EXISTS_VAL_FAILED update client %1 for zone %2: 'RRset exists (value dependent)' (%3) prerequisite not satisfied, rcode: %4</term>
+<listitem><para>
+A DNS UPDATE prerequisite was not satisfied. The specific prerequisite that
+was not satisfied is shown. The client is sent an error response with the
+given rcode.
+In this case, the specific prerequisite is 'RRset exists (value dependent)'.
+From RFC2136:
+RRset exists (value independent).  At least one RR with a
+specified NAME and TYPE (in the zone and class specified by
+the Zone Section) must exist.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_ADD_BAD_TYPE">
+<term>LIBDDNS_UPDATE_ADD_BAD_TYPE update client %1 for zone %2: update addition RR bad type: %3</term>
+<listitem><para>
+The Update section of a DDNS update message contains a statement
+that tries to add a record of an invalid type. Most likely the
+record has an RRType that is considered a 'meta' type, which
+cannot be zone content data. The specific record is shown.
+A FORMERR response is sent back to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_APPROVED">
+<term>LIBDDNS_UPDATE_APPROVED update client %1 for zone %2 approved</term>
+<listitem><para>
+Debug message.  An update request was approved in terms of the zone's
+update ACL.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_BAD_CLASS">
+<term>LIBDDNS_UPDATE_BAD_CLASS update client %1 for zone %2: bad class in update RR: %3</term>
+<listitem><para>
+The Update section of a DDNS update message contains an RRset with
+a bad class. The class of the update RRset must be either the same
+as the class in the Zone Section, ANY, or NONE.
+A FORMERR response is sent back to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_DATASRC_ERROR">
+<term>LIBDDNS_UPDATE_DATASRC_ERROR error in datasource during DDNS update: %1</term>
+<listitem><para>
+An error occured while committing the DDNS update changes to the
+datasource. The specific error is printed. A SERVFAIL response is sent
+back to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_DELETE_BAD_TYPE">
+<term>LIBDDNS_UPDATE_DELETE_BAD_TYPE update client %1 for zone %2: update deletion RR bad type: %3</term>
+<listitem><para>
+The Update section of a DDNS update message contains a statement
+that tries to delete an rrset of an invalid type. Most likely the
+record has an RRType that is considered a 'meta' type, which
+cannot be zone content data. The specific record is shown.
+A FORMERR response is sent back to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_DELETE_NONZERO_TTL">
+<term>LIBDDNS_UPDATE_DELETE_NONZERO_TTL update client %1 for zone %2: update deletion RR has non-zero TTL: %3</term>
+<listitem><para>
+The Update section of a DDNS update message contains a 'delete rrset'
+statement with a non-zero TTL. This is not allowed by the protocol.
+A FORMERR response is sent back to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_DELETE_RRSET_NOT_EMPTY">
+<term>LIBDDNS_UPDATE_DELETE_RRSET_NOT_EMPTY update client %1 for zone %2: update deletion RR contains data %3</term>
+<listitem><para>
+The Update section of a DDNS update message contains a 'delete rrset'
+statement with a non-empty RRset. This is not allowed by the protocol.
+A FORMERR response is sent back to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_DELETE_RR_BAD_TYPE">
+<term>LIBDDNS_UPDATE_DELETE_RR_BAD_TYPE update client %1 for zone %2: update deletion RR bad type: %3</term>
+<listitem><para>
+The Update section of a DDNS update message contains a statement
+that tries to delete one or more rrs of an invalid type. Most
+likely the records have an RRType that is considered a 'meta'
+type, which cannot be zone content data. The specific record is
+shown. A FORMERR response is sent back to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_DELETE_RR_NONZERO_TTL">
+<term>LIBDDNS_UPDATE_DELETE_RR_NONZERO_TTL update client %1 for zone %2: update deletion RR has non-zero TTL: %3</term>
+<listitem><para>
+The Update section of a DDNS update message contains a 'delete rrs'
+statement with a non-zero TTL. This is not allowed by the protocol.
+A FORMERR response is sent back to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_DENIED">
+<term>LIBDDNS_UPDATE_DENIED update client %1 for zone %2 denied</term>
+<listitem><para>
+Informational message.  An update request was denied because it was
+rejected by the zone's update ACL.  When this library is used by
+b10-ddns, the server will respond to the request with an RCODE of
+REFUSED as described in Section 3.3 of RFC2136.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_DROPPED">
+<term>LIBDDNS_UPDATE_DROPPED update client %1 for zone %2 dropped</term>
+<listitem><para>
+Informational message.  An update request was denied because it was
+rejected by the zone's update ACL.  When this library is used by
+b10-ddns, the server will then completely ignore the request; no
+response will be sent.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_ERROR">
+<term>LIBDDNS_UPDATE_ERROR update client %1 for zone %2: %3</term>
+<listitem><para>
+Debug message.  An error is found in processing a dynamic update
+request.  This log message is used for general errors that are not
+normally expected to happen.  So, in general, it would mean some
+problem in the client implementation or an interoperability issue
+with this implementation.  The client's address, the zone name and
+class, and description of the error are logged.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_FORWARD_FAIL">
+<term>LIBDDNS_UPDATE_FORWARD_FAIL update client %1 for zone %2: update forwarding not supported</term>
+<listitem><para>
+Debug message.  An update request is sent to a secondary server.  This
+is not necessarily invalid, but this implementation does not yet
+support update forwarding as specified in Section 6 of RFC2136 and it
+will simply return a response with an RCODE of NOTIMP to the client.
+The client's address and the zone name/class are logged.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_NOTAUTH">
+<term>LIBDDNS_UPDATE_NOTAUTH update client %1 for zone %2: not authoritative for update zone</term>
+<listitem><para>
+Debug message.  An update request was received for a zone for which
+the receiving server doesn't have authority.  In theory this is an
+unexpected event, but there are client implementations that could send
+update requests carelessly, so it may not necessarily be so uncommon
+in practice.  If possible, you may want to check the implementation or
+configuration of those clients to suppress the requests.  As specified
+in Section 3.1 of RFC2136, the receiving server will return a response
+with an RCODE of NOTAUTH.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_NOTZONE">
+<term>LIBDDNS_UPDATE_NOTZONE update client %1 for zone %2: update RR out of zone %3</term>
+<listitem><para>
+A DDNS UPDATE record has a name that does not appear to be inside
+the zone specified in the Zone section of the UPDATE message.
+The specific update record is shown. A NOTZONE error response is
+sent to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_PREREQUISITE_FAILED">
+<term>LIBDDNS_UPDATE_PREREQUISITE_FAILED prerequisite failed in update client %1 for zone %2: result code %3</term>
+<listitem><para>
+The handling of the prerequisite section (RFC2136 Section 3.2) found
+that one of the prerequisites was not satisfied. The result code
+should give more information on what prerequisite type failed.
+If the result code is FORMERR, the prerequisite section was not well-formed.
+An error response with the given result code is sent back to the client.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="LIBDDNS_UPDATE_UNCAUGHT_EXCEPTION">
+<term>LIBDDNS_UPDATE_UNCAUGHT_EXCEPTION update client %1 for zone %2: uncaught exception while processing update section: %3</term>
+<listitem><para>
+An uncaught exception was encountered while processing the Update
+section of a DDNS message. The specific exception is shown in the log message.
+To make sure DDNS service is not interrupted, this problem is caught instead
+of reraised; The update is aborted, and a SERVFAIL is sent back to the client.
+This is most probably a bug in the DDNS code, but *could* be caused by
+the data source.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="LIBXFRIN_DIFFERENT_TTL">
 <term>LIBXFRIN_DIFFERENT_TTL multiple data with different TTLs (%1, %2) on %3/%4/%5. Adjusting %2 -&gt; %1.</term>
 <listitem><para>
@@ -3750,6 +4383,13 @@ and the underscore, and should not start with a digit.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="LOG_LOCK_TEST_MESSAGE">
+<term>LOG_LOCK_TEST_MESSAGE this is a test message.</term>
+<listitem><para>
+This is a log message used in testing.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="LOG_NAMESPACE_EXTRA_ARGS">
 <term>LOG_NAMESPACE_EXTRA_ARGS line %1: $NAMESPACE directive has too many arguments</term>
 <listitem><para>
@@ -4141,6 +4781,55 @@ bug report.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="PYSERVER_COMMON_AUTH_CONFIG_NAME_PARSER_ERROR">
+<term>PYSERVER_COMMON_AUTH_CONFIG_NAME_PARSER_ERROR Invalid name when parsing Auth configuration: %1</term>
+<listitem><para>
+There was an invalid name when parsing Auth configuration.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="PYSERVER_COMMON_AUTH_CONFIG_RRCLASS_ERROR">
+<term>PYSERVER_COMMON_AUTH_CONFIG_RRCLASS_ERROR Invalid RRClass when parsing Auth configuration: %1</term>
+<listitem><para>
+There was an invalid RR class when parsing Auth configuration.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="PYSERVER_COMMON_DNS_TCP_SEND_DONE">
+<term>PYSERVER_COMMON_DNS_TCP_SEND_DONE completed sending TCP message to %1 (%2 bytes in total)</term>
+<listitem><para>
+Debug message.  A complete DNS message has been successfully
+transmitted over a TCP connection, possibly after multiple send
+operations.  The destination address and the total size of the message
+(including the 2-byte length field) are shown in the log message.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="PYSERVER_COMMON_DNS_TCP_SEND_ERROR">
+<term>PYSERVER_COMMON_DNS_TCP_SEND_ERROR failed to send TCP message to %1 (%2/%3 bytes sent): %4</term>
+<listitem><para>
+A DNS message has been attempted to be sent out over a TCP connection,
+but it failed due to some network error.  Although it's not expected
+to happen too often, it can still happen for various reasons.  The
+administrator may want to examine the cause of the failure, which is
+included in the log message, to see if it requires some action to
+be taken at the server side.  When this message is logged, the
+corresponding  TCP connection was closed immediately after the error
+was detected.
+</para></listitem>
+</varlistentry>
+
+<varlistentry id="PYSERVER_COMMON_DNS_TCP_SEND_PENDING">
+<term>PYSERVER_COMMON_DNS_TCP_SEND_PENDING sent part TCP message to %1 (up to %2/%3 bytes)</term>
+<listitem><para>
+Debug message.  A part of DNS message has been transmitted over a TCP
+connection, and it's suspended because further attempt would block.
+The destination address and the total size of the message that has
+been transmitted so far (including the 2-byte length field) are shown
+in the log message.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="PYSERVER_COMMON_TSIG_KEYRING_DEINIT">
 <term>PYSERVER_COMMON_TSIG_KEYRING_DEINIT Deinitializing global TSIG keyring</term>
 <listitem><para>
@@ -5388,6 +6077,15 @@ Please check your installation.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="XFRIN_AUTH_LOADZONE">
+<term>XFRIN_AUTH_LOADZONE sending Auth loadzone for origin=%1, class=%2, datasrc=%3</term>
+<listitem><para>
+There was a successful zone transfer, and the zone is served by b10-auth
+in the in-memory data source using sqlite3 as a backend. We send the
+"loadzone" command for the zone to b10-auth.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="XFRIN_AXFR_INCONSISTENT_SOA">
 <term>XFRIN_AXFR_INCONSISTENT_SOA AXFR SOAs are inconsistent for %1: %2 expected, %3 received</term>
 <listitem><para>
@@ -5542,6 +6240,14 @@ was killed.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="XFRIN_MSGQ_SEND_ERROR_AUTH">
+<term>XFRIN_MSGQ_SEND_ERROR_AUTH error while contacting %1</term>
+<listitem><para>
+There was a problem sending a message to b10-auth. This most likely
+means that the msgq daemon has quit or was killed.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="XFRIN_MSGQ_SEND_ERROR_ZONE_MANAGER">
 <term>XFRIN_MSGQ_SEND_ERROR_ZONE_MANAGER error while contacting %1</term>
 <listitem><para>
@@ -5570,10 +6276,11 @@ zone name in the configuration.
 </para></listitem>
 </varlistentry>
 
-<varlistentry id="XFRIN_STARTING">
-<term>XFRIN_STARTING starting resolver with command line '%1'</term>
+<varlistentry id="XFRIN_STARTED">
+<term>XFRIN_STARTED xfrin started</term>
 <listitem><para>
-An informational message, this is output when the resolver starts up.
+This informational message is output by xfrin when all initialization
+has been completed and it is entering its main loop.
 </para></listitem>
 </varlistentry>
 
@@ -5909,12 +6616,16 @@ parsed by the b10-auth daemon, before it was passed here.
 </varlistentry>
 
 <varlistentry id="XFROUT_PROCESS_REQUEST_ERROR">
-<term>XFROUT_PROCESS_REQUEST_ERROR error processing transfer request: %2</term>
+<term>XFROUT_PROCESS_REQUEST_ERROR error processing transfer request: %1</term>
 <listitem><para>
-There was an error processing a transfer request. The error is included
-in the log message, but at this point no specific information other
-than that could be given. This points to incomplete exception handling
-in the code.
+There was an error in receiving a transfer request from b10-auth.
+This is generally an unexpected event, but is possible when, for
+example, b10-auth terminates in the middle of forwarding the request.
+When this happens it's unlikely to be recoverable with the same
+communication session with b10-auth, so b10-xfrout drops it and
+waits for a new session.  In any case, this error indicates that
+there's something very wrong in the system, so it's advisable to check
+the over all status of the BIND 10 system.
 </para></listitem>
 </varlistentry>
 
@@ -5964,9 +6675,16 @@ and will now shut down.
 <term>XFROUT_RECEIVE_FILE_DESCRIPTOR_ERROR error receiving the file descriptor for an XFR connection</term>
 <listitem><para>
 There was an error receiving the file descriptor for the transfer
-request. Normally, the request is received by b10-auth, and passed on
-to the xfrout daemon, so it can answer directly. However, there was a
-problem receiving this file descriptor. The request will be ignored.
+request from b10-auth.  There can be several reasons for this, but
+the most likely cause is that b10-auth terminates for some reason
+(maybe it's a bug of b10-auth, maybe it's an intentional restart by
+the administrator), so depending on how this happens it may or may not
+be a serious error.  But in any case this is not expected to happen
+frequently, and it's advisable to figure out how this happened if
+this message is logged.  Even if this error happens xfrout will reset
+its internal state and will keep receiving further requests.  So
+if it's just a temporary restart of b10-auth the administrator does
+not have to do anything.
 </para></listitem>
 </varlistentry>
 
@@ -6001,6 +6719,14 @@ log message.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="XFROUT_STARTED">
+<term>XFROUT_STARTED xfrout started</term>
+<listitem><para>
+This informational message is output by xfrout when all initialization
+has been completed and it is entering its main loop.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="XFROUT_STOPPED_BY_KEYBOARD">
 <term>XFROUT_STOPPED_BY_KEYBOARD keyboard interrupt, shutting down</term>
 <listitem><para>
@@ -6257,6 +6983,14 @@ A debug message, output when the zone manager has shut down completely.
 </para></listitem>
 </varlistentry>
 
+<varlistentry id="ZONEMGR_STARTED">
+<term>ZONEMGR_STARTED zonemgr started</term>
+<listitem><para>
+This informational message is output by zonemgr when all initialization
+has been completed and it is entering its main loop.
+</para></listitem>
+</varlistentry>
+
 <varlistentry id="ZONEMGR_STARTING">
 <term>ZONEMGR_STARTING zone manager starting</term>
 <listitem><para>

+ 5 - 0
src/Makefile.am

@@ -1 +1,6 @@
 SUBDIRS = lib bin
+
+EXTRA_DIST = \
+	cppcheck-suppress.lst		\
+	valgrind-suppressions		\
+	valgrind-suppressions.revisit

+ 4 - 3
src/bin/auth/Makefile.am

@@ -51,9 +51,9 @@ b10_auth_SOURCES += statistics.cc statistics.h
 b10_auth_SOURCES += main.cc
 # This is a temporary workaround for #1206, where the InMemoryClient has been
 # moved to an ldopened library. We could add that library to LDADD, but that
-# is nonportable. When #1207 is done this becomes moot anyway, and the
-# specific workaround is not needed anymore, so we can then remove this
-# line again.
+# is nonportable. This should've been moot after #1207, but there is still
+# one dependency; the in-memory-specific zone loader call is still in
+# auth.
 b10_auth_SOURCES += ${top_srcdir}/src/lib/datasrc/memory_datasrc.cc
 
 nodist_b10_auth_SOURCES = auth_messages.h auth_messages.cc
@@ -62,6 +62,7 @@ EXTRA_DIST += auth_messages.mes
 b10_auth_LDADD =  $(top_builddir)/src/lib/datasrc/libdatasrc.la
 b10_auth_LDADD += $(top_builddir)/src/lib/dns/libdns++.la
 b10_auth_LDADD += $(top_builddir)/src/lib/util/libutil.la
+b10_auth_LDADD += $(top_builddir)/src/lib/util/io/libutil_io.la
 b10_auth_LDADD += $(top_builddir)/src/lib/config/libcfgclient.la
 b10_auth_LDADD += $(top_builddir)/src/lib/cc/libcc.la
 b10_auth_LDADD += $(top_builddir)/src/lib/exceptions/libexceptions.la

+ 52 - 143
src/bin/auth/auth_config.cc

@@ -43,22 +43,19 @@ using namespace isc::datasrc;
 using namespace isc::server_common::portconfig;
 
 namespace {
-// Forward declaration
-AuthConfigParser*
-createAuthConfigParser(AuthSrv& server, const std::string& config_id,
-                       bool internal);
-
 /// A derived \c AuthConfigParser class for the "datasources" configuration
 /// identifier.
 class DatasourcesConfig : public AuthConfigParser {
 public:
-    DatasourcesConfig(AuthSrv& server) : server_(server) {}
+    DatasourcesConfig(AuthSrv& server) : server_(server)
+    {}
     virtual void build(ConstElementPtr config_value);
     virtual void commit();
 private:
     AuthSrv& server_;
     vector<boost::shared_ptr<AuthConfigParser> > datasources_;
     set<string> configured_sources_;
+    vector<pair<RRClass, DataSourceClientContainerPtr> > clients_;
 };
 
 /// A derived \c AuthConfigParser for the version value
@@ -86,137 +83,60 @@ DatasourcesConfig::build(ConstElementPtr config_value) {
             isc_throw(AuthConfigError, "Data source type '" <<
                       datasrc_type->stringValue() << "' already configured");
         }
-        
-        boost::shared_ptr<AuthConfigParser> datasrc_config =
-            boost::shared_ptr<AuthConfigParser>(
-                createAuthConfigParser(server_, string("datasources/") +
-                                       datasrc_type->stringValue(),
-                                       true));
-        datasrc_config->build(datasrc_elem);
-        datasources_.push_back(datasrc_config);
 
-        configured_sources_.insert(datasrc_type->stringValue());
-    }
-}
+        // Apart from that it's not really easy to get at the default
+        // class value for the class here, it should probably really
+        // be a property of the instantiated data source. For now
+        // use hardcoded default IN.
+        const RRClass rrclass =
+            datasrc_elem->contains("class") ?
+            RRClass(datasrc_elem->get("class")->stringValue()) : RRClass::IN();
+
+        // Right now, we only support the in-memory data source for the
+        // RR class of IN.  We reject other cases explicitly by hardcoded
+        // checks.  This will soon be generalized, at which point these
+        // checks will also have to be cleaned up.
+        if (rrclass != RRClass::IN()) {
+            isc_throw(isc::InvalidParameter, "Unsupported data source class: "
+                      << rrclass);
+        }
+        if (datasrc_type->stringValue() != "memory") {
+            isc_throw(AuthConfigError, "Unsupported data source type: "
+                      << datasrc_type->stringValue());
+        }
 
-void
-DatasourcesConfig::commit() {
-    // XXX a short term workaround: clear all data sources and then reset
-    // to new ones so that we can remove data sources that don't exist in
-    // the new configuration and have been used in the server.
-    // This could be inefficient and requires knowledge about
-    // server implementation details, and isn't scalable wrt the number of
-    // data source types, and should eventually be improved.
-    // Currently memory data source for class IN is the only possibility.
-    server_.setInMemoryClient(RRClass::IN(), AuthSrv::InMemoryClientPtr());
+        // Create a new client for the specified data source and store it
+        // in the local vector.  For now, we always build a new client
+        // from the scratch, and replace any existing ones with the new ones.
+        // We might eventually want to optimize building zones (in case of
+        // reloading) by selectively loading fresh zones for data source
+        // where zone loading is expensive (such as in-memory).
+        clients_.push_back(
+            pair<RRClass, DataSourceClientContainerPtr>(
+                rrclass,
+                DataSourceClientContainerPtr(new DataSourceClientContainer(
+                                                 datasrc_type->stringValue(),
+                                                 datasrc_elem))));
 
-    BOOST_FOREACH(boost::shared_ptr<AuthConfigParser> datasrc_config,
-                  datasources_) {
-        datasrc_config->commit();
+        configured_sources_.insert(datasrc_type->stringValue());
     }
 }
 
-/// A derived \c AuthConfigParser class for the memory type datasource
-/// configuration.  It does not correspond to the configuration syntax;
-/// it's instantiated for internal use.
-class MemoryDatasourceConfig : public AuthConfigParser {
-public:
-    MemoryDatasourceConfig(AuthSrv& server) :
-        server_(server),
-        rrclass_(0)              // XXX: dummy initial value
-    {}
-    virtual void build(ConstElementPtr config_value);
-    virtual void commit() {
-        server_.setInMemoryClient(rrclass_, memory_client_);
-    }
-private:
-    AuthSrv& server_;
-    RRClass rrclass_;
-    AuthSrv::InMemoryClientPtr memory_client_;
-};
-
 void
-MemoryDatasourceConfig::build(ConstElementPtr config_value) {
-    // XXX: apparently we cannot retrieve the default RR class from the
-    // module spec.  As a temporary workaround we hardcode the default value.
-    ConstElementPtr rrclass_elem = config_value->get("class");
-    rrclass_ = RRClass(rrclass_elem ? rrclass_elem->stringValue() : "IN");
-
-    // We'd eventually optimize building zones (in case of reloading) by
-    // selectively loading fresh zones.  Right now we simply check the
-    // RR class is supported by the server implementation.
-    server_.getInMemoryClient(rrclass_);
-    memory_client_ = AuthSrv::InMemoryClientPtr(new InMemoryClient());
-
-    ConstElementPtr zones_config = config_value->get("zones");
-    if (!zones_config) {
-        // XXX: Like the RR class, we cannot retrieve the default value here,
-        // so we assume an empty zone list in this case.
-        return;
-    }
-
-    BOOST_FOREACH(ConstElementPtr zone_config, zones_config->listValue()) {
-        ConstElementPtr origin = zone_config->get("origin");
-        const string origin_txt = origin ? origin->stringValue() : "";
-        if (origin_txt.empty()) {
-            isc_throw(AuthConfigError, "Missing zone origin");
-        }
-        ConstElementPtr file = zone_config->get("file");
-        const string file_txt = file ? file->stringValue() : "";
-        if (file_txt.empty()) {
-            isc_throw(AuthConfigError, "Missing zone file for zone: "
-                      << origin_txt);
-        }
-
-        // We support the traditional text type and SQLite3 backend.  For the
-        // latter we create a client for the underlying SQLite3 data source,
-        // and build the in-memory zone using an iterator of the underlying
-        // zone.
-        ConstElementPtr filetype = zone_config->get("filetype");
-        const string filetype_txt = filetype ? filetype->stringValue() :
-            "text";
-        boost::scoped_ptr<DataSourceClientContainer> container;
-        if (filetype_txt == "sqlite3") {
-            container.reset(new DataSourceClientContainer(
-                                "sqlite3",
-                                Element::fromJSON("{\"database_file\": \"" +
-                                                  file_txt + "\"}")));
-        } else if (filetype_txt != "text") {
-            isc_throw(AuthConfigError, "Invalid filetype for zone "
-                      << origin_txt << ": " << filetype_txt);
-        }
-
-        // Note: we don't want to have such small try-catch blocks for each
-        // specific error.  We may eventually want to introduce some unified
-        // error handling framework as we have more configuration parameters.
-        // See bug #1627 for the relevant discussion.
-        InMemoryZoneFinder* imzf = NULL;
-        try {
-            imzf = new InMemoryZoneFinder(rrclass_, Name(origin_txt));
-        } catch (const isc::dns::NameParserException& ex) {
-            isc_throw(AuthConfigError, "unable to parse zone's origin: " <<
-                      ex.what());
-        }
-
-        boost::shared_ptr<InMemoryZoneFinder> zone_finder(imzf);
-        const result::Result result = memory_client_->addZone(zone_finder);
-        if (result == result::EXIST) {
-            isc_throw(AuthConfigError, "zone "<< origin->str()
-                      << " already exists");
-        }
-
-        /*
-         * TODO: Once we have better reloading of configuration (something
-         * else than throwing everything away and loading it again), we will
-         * need the load method to be split into some kind of build and
-         * commit/abort parts.
-         */
-        if (filetype_txt == "text") {
-            zone_finder->load(file_txt);
-        } else {
-            zone_finder->load(*container->getInstance().getIterator(
-                                  Name(origin_txt)));
-        }
+DatasourcesConfig::commit() {
+    // As noted in build(), the current implementation only supports the
+    // in-memory data source for class IN, and build() should have ensured
+    // it.  So, depending on the vector is empty or not, we either clear
+    // or install an in-memory data source for the server.
+    //
+    // When we generalize it, we'll somehow install all data source clients
+    // built in the vector, clearing deleted ones from the server.
+    if (clients_.empty()) {
+        server_.setInMemoryClient(RRClass::IN(),
+                                  DataSourceClientContainerPtr());
+    } else {
+        server_.setInMemoryClient(clients_.front().first,
+                                  clients_.front().second);
     }
 }
 
@@ -314,13 +234,10 @@ private:
      */
     AddrListPtr rollbackAddresses_;
 };
+} // end of unnamed namespace
 
-// This is a generalized version of create function that can create
-// an AuthConfigParser object for "internal" use.
 AuthConfigParser*
-createAuthConfigParser(AuthSrv& server, const std::string& config_id,
-                       bool internal)
-{
+createAuthConfigParser(AuthSrv& server, const std::string& config_id) {
     // For the initial implementation we use a naive if-else blocks for
     // simplicity.  In future we'll probably generalize it using map-like
     // data structure, and may even provide external register interface so
@@ -329,8 +246,6 @@ createAuthConfigParser(AuthSrv& server, const std::string& config_id,
         return (new DatasourcesConfig(server));
     } else if (config_id == "statistics-interval") {
         return (new StatisticsIntervalConfig(server));
-    } else if (internal && config_id == "datasources/memory") {
-        return (new MemoryDatasourceConfig(server));
     } else if (config_id == "listen_on") {
         return (new ListenAddressConfig(server));
     } else if (config_id == "_commit_throw") {
@@ -351,12 +266,6 @@ createAuthConfigParser(AuthSrv& server, const std::string& config_id,
                   config_id);
     }
 }
-} // end of unnamed namespace
-
-AuthConfigParser*
-createAuthConfigParser(AuthSrv& server, const std::string& config_id) {
-    return (createAuthConfigParser(server, config_id, false));
-}
 
 void
 configureAuthServer(AuthSrv& server, ConstElementPtr config_set) {

+ 14 - 0
src/bin/auth/auth_messages.mes

@@ -96,6 +96,20 @@ discovered that the memory data source is disabled for the given class.
 This is a debug message reporting that the authoritative server has
 discovered that the memory data source is enabled for the given class.
 
+% AUTH_MESSAGE_FORWARD_ERROR failed to forward %1 request from %2: %3
+The authoritative server tried to forward some type DNS request
+message to a separate process (e.g., forwarding dynamic update
+requests to b10-ddns) to handle it, but it failed.  The authoritative
+server returns SERVFAIL to the client on behalf of the separate
+process.  The error could be configuration mismatch between b10-auth
+and the recipient component, or it may be because the requests are
+coming too fast and the receipient process cannot keep up with the
+rate, or some system level failure.  In either case this means the
+BIND 10 system is not working as expected, so the administrator should
+look into the cause and address the issue.  The log message includes
+the client's address (and port), and the error message sent from the
+lower layer that detects the failure.
+
 % AUTH_NOTIFY_QUESTIONS invalid number of questions (%1) in incoming NOTIFY
 This debug message is logged by the authoritative server when it receives
 a NOTIFY packet that contains zero or more than one question. (A valid

+ 171 - 30
src/bin/auth/auth_srv.cc

@@ -14,18 +14,10 @@
 
 #include <config.h>
 
-#include <sys/types.h>
-#include <netinet/in.h>
-
-#include <algorithm>
-#include <cassert>
-#include <iostream>
-#include <vector>
-#include <memory>
-
-#include <boost/bind.hpp>
+#include <util/io/socketsession.h>
 
 #include <asiolink/asiolink.h>
+#include <asiolink/io_endpoint.h>
 
 #include <config/ccsession.h>
 
@@ -64,6 +56,18 @@
 #include <auth/statistics.h>
 #include <auth/auth_log.h>
 
+#include <boost/bind.hpp>
+#include <boost/lexical_cast.hpp>
+
+#include <algorithm>
+#include <cassert>
+#include <iostream>
+#include <vector>
+#include <memory>
+
+#include <sys/types.h>
+#include <netinet/in.h>
+
 using namespace std;
 
 using namespace isc;
@@ -71,6 +75,7 @@ using namespace isc::cc;
 using namespace isc::datasrc;
 using namespace isc::dns;
 using namespace isc::util;
+using namespace isc::util::io;
 using namespace isc::auth;
 using namespace isc::dns::rdata;
 using namespace isc::data;
@@ -107,6 +112,107 @@ public:
 private:
     MessageRenderer& renderer_;
 };
+
+// A helper container of socket session forwarder.
+//
+// This class provides a simple wrapper interface to SocketSessionForwarder
+// so that the caller doesn't have to worry about connection management,
+// exception handling or parameter building.
+//
+// It internally maintains whether the underlying forwarder establishes a
+// connection to the receiver.  On a forwarding request, if the connection
+// hasn't been established yet, it automatically opens a new one, then
+// pushes the session over it.  It also closes the connection on destruction,
+// or a non-recoverable error happens, automatically.  So the only thing
+// the application has to do is to create this object and push any session
+// to be forwarded.
+class SocketSessionForwarderHolder {
+public:
+    /// \brief The constructor.
+    ///
+    /// \param message_name Any string that can identify the type of messages
+    /// to be forwarded via this session.  It will be only used as part of
+    /// log message, so it can be anything, but in practice something like
+    /// "update" or "xfr" is expected.
+    /// \param forwarder The underlying socket session forwarder.
+    SocketSessionForwarderHolder(const string& message_name,
+                                 BaseSocketSessionForwarder& forwarder) :
+        message_name_(message_name), forwarder_(forwarder), connected_(false)
+    {}
+
+    ~SocketSessionForwarderHolder() {
+        if (connected_) {
+            forwarder_.close();
+        }
+    }
+
+    /// \brief Push a socket session corresponding to given IOMessage.
+    ///
+    /// If the connection with the receiver process hasn't been established,
+    /// it automatically establishes one, then push the session over it.
+    ///
+    /// If either connect or push fails, the underlying forwarder object should
+    /// throw an exception.  This method logs the event, and propagates the
+    /// exception to the caller, which will eventually result in SERVFAIL.
+    /// The connection, if established, is automatically closed, so the next
+    /// forward request will trigger reopening a new connection.
+    ///
+    /// \note: Right now, there's no API to retrieve the local address from
+    /// the IOMessage.  Until it's added, we pass the remote address as
+    /// local.
+    ///
+    /// \param io_message The request message to be forwarded as a socket
+    /// session.  It will be converted to the parameters that the underlying
+    /// SocketSessionForwarder expects.
+    void push(const IOMessage& io_message) {
+        const IOEndpoint& remote_ep = io_message.getRemoteEndpoint();
+        const int protocol = remote_ep.getProtocol();
+        const int sock_type = getSocketType(protocol);
+        try {
+            connect();
+            forwarder_.push(io_message.getSocket().getNative(),
+                            remote_ep.getFamily(), sock_type, protocol,
+                            remote_ep.getSockAddr(), remote_ep.getSockAddr(),
+                            io_message.getData(), io_message.getDataSize());
+        } catch (const SocketSessionError& ex) {
+            LOG_ERROR(auth_logger, AUTH_MESSAGE_FORWARD_ERROR).
+                arg(message_name_).arg(remote_ep).arg(ex.what());
+            close();
+            throw;
+        }
+    }
+
+private:
+    const string message_name_;
+    BaseSocketSessionForwarder& forwarder_;
+    bool connected_;
+
+    void connect() {
+        if (!connected_) {
+            forwarder_.connectToReceiver();
+            connected_ = true;
+        }
+    }
+
+    void close() {
+        if (connected_) {
+            forwarder_.close();
+            connected_ = false;
+        }
+    }
+
+    static int getSocketType(int protocol) {
+        switch (protocol) {
+        case IPPROTO_UDP:
+            return (SOCK_DGRAM);
+        case IPPROTO_TCP:
+            return (SOCK_STREAM);
+        default:
+            isc_throw(isc::InvalidParameter,
+                      "Unexpected socket address family: " << protocol);
+        }
+    }
+};
 }
 
 class AuthSrvImpl {
@@ -115,7 +221,8 @@ private:
     AuthSrvImpl(const AuthSrvImpl& source);
     AuthSrvImpl& operator=(const AuthSrvImpl& source);
 public:
-    AuthSrvImpl(const bool use_cache, AbstractXfroutClient& xfrout_client);
+    AuthSrvImpl(const bool use_cache, AbstractXfroutClient& xfrout_client,
+                BaseSocketSessionForwarder& ddns_forwarder);
     ~AuthSrvImpl();
     isc::data::ConstElementPtr setDbFile(isc::data::ConstElementPtr config);
 
@@ -128,6 +235,7 @@ public:
     bool processNotify(const IOMessage& io_message, Message& message,
                        OutputBuffer& buffer,
                        auto_ptr<TSIGContext> tsig_context);
+    bool processUpdate(const IOMessage& io_message);
 
     IOService io_service_;
 
@@ -141,7 +249,7 @@ public:
 
     /// In-memory data source.  Currently class IN only for simplicity.
     const RRClass memory_client_class_;
-    AuthSrv::InMemoryClientPtr memory_client_;
+    isc::datasrc::DataSourceClientContainerPtr memory_client_container_;
 
     /// Hot spot cache
     isc::datasrc::HotCache cache_;
@@ -189,6 +297,9 @@ private:
     bool xfrout_connected_;
     AbstractXfroutClient& xfrout_client_;
 
+    // Socket session forwarder for dynamic update requests
+    SocketSessionForwarderHolder ddns_forwarder_;
+
     /// Increment query counter
     void incCounter(const int protocol);
 
@@ -199,7 +310,8 @@ private:
 };
 
 AuthSrvImpl::AuthSrvImpl(const bool use_cache,
-                         AbstractXfroutClient& xfrout_client) :
+                         AbstractXfroutClient& xfrout_client,
+                         BaseSocketSessionForwarder& ddns_forwarder) :
     config_session_(NULL),
     xfrin_session_(NULL),
     memory_client_class_(RRClass::IN()),
@@ -207,7 +319,8 @@ AuthSrvImpl::AuthSrvImpl(const bool use_cache,
     counters_(),
     keyring_(NULL),
     xfrout_connected_(false),
-    xfrout_client_(xfrout_client)
+    xfrout_client_(xfrout_client),
+    ddns_forwarder_("update", ddns_forwarder)
 {
     // cur_datasrc_ is automatically initialized by the default constructor,
     // effectively being an empty (sqlite) data source.  once ccsession is up
@@ -277,9 +390,11 @@ private:
     AuthSrv* server_;
 };
 
-AuthSrv::AuthSrv(const bool use_cache, AbstractXfroutClient& xfrout_client)
+AuthSrv::AuthSrv(const bool use_cache,
+                 isc::xfr::AbstractXfroutClient& xfrout_client,
+                 isc::util::io::BaseSocketSessionForwarder& ddns_forwarder)
 {
-    impl_ = new AuthSrvImpl(use_cache, xfrout_client);
+    impl_ = new AuthSrvImpl(use_cache, xfrout_client, ddns_forwarder);
     checkin_ = new ConfigChecker(this);
     dns_lookup_ = new MessageLookup(this);
     dns_answer_ = new MessageAnswer(this);
@@ -389,34 +504,46 @@ AuthSrv::getConfigSession() const {
     return (impl_->config_session_);
 }
 
-AuthSrv::InMemoryClientPtr
-AuthSrv::getInMemoryClient(const RRClass& rrclass) {
-    // XXX: for simplicity, we only support the IN class right now.
+isc::datasrc::DataSourceClientContainerPtr
+AuthSrv::getInMemoryClientContainer(const RRClass& rrclass) {
     if (rrclass != impl_->memory_client_class_) {
         isc_throw(InvalidParameter,
                   "Memory data source is not supported for RR class "
                   << rrclass);
     }
-    return (impl_->memory_client_);
+    return (impl_->memory_client_container_);
+}
+
+isc::datasrc::DataSourceClient*
+AuthSrv::getInMemoryClient(const RRClass& rrclass) {
+    if (hasInMemoryClient()) {
+        return (&getInMemoryClientContainer(rrclass)->getInstance());
+    } else {
+        return (NULL);
+    }
+}
+
+bool
+AuthSrv::hasInMemoryClient() const {
+    return (impl_->memory_client_container_);
 }
 
 void
 AuthSrv::setInMemoryClient(const isc::dns::RRClass& rrclass,
-                           InMemoryClientPtr memory_client)
+                           DataSourceClientContainerPtr memory_client)
 {
-    // XXX: see above
     if (rrclass != impl_->memory_client_class_) {
         isc_throw(InvalidParameter,
                   "Memory data source is not supported for RR class "
                   << rrclass);
-    } else if (!impl_->memory_client_ && memory_client) {
+    } else if (!impl_->memory_client_container_ && memory_client) {
         LOG_DEBUG(auth_logger, DBG_AUTH_OPS, AUTH_MEM_DATASRC_ENABLED)
                   .arg(rrclass);
-    } else if (impl_->memory_client_ && !memory_client) {
+    } else if (impl_->memory_client_container_ && !memory_client) {
         LOG_DEBUG(auth_logger, DBG_AUTH_OPS, AUTH_MEM_DATASRC_DISABLED)
                   .arg(rrclass);
     }
-    impl_->memory_client_ = memory_client;
+    impl_->memory_client_container_ = memory_client;
 }
 
 uint32_t
@@ -515,16 +642,19 @@ AuthSrv::processMessage(const IOMessage& io_message, Message& message,
         return;
     }
 
+    const Opcode opcode = message.getOpcode();
     bool send_answer = true;
     try {
         // update per opcode statistics counter.  This can only be reliable
         // after TSIG check succeeds.
         impl_->counters_.inc(message.getOpcode());
 
-        if (message.getOpcode() == Opcode::NOTIFY()) {
+        if (opcode == Opcode::NOTIFY()) {
             send_answer = impl_->processNotify(io_message, message, buffer,
                                                tsig_context);
-        } else if (message.getOpcode() != Opcode::QUERY()) {
+        } else if (opcode == Opcode::UPDATE()) {
+            send_answer = impl_->processUpdate(io_message);
+        } else if (opcode != Opcode::QUERY()) {
             LOG_DEBUG(auth_logger, DBG_AUTH_DETAIL, AUTH_UNSUPPORTED_OPCODE)
                       .arg(message.getOpcode().toText());
             makeErrorMessage(impl_->renderer_, message, buffer,
@@ -534,7 +664,7 @@ AuthSrv::processMessage(const IOMessage& io_message, Message& message,
                              Rcode::FORMERR(), tsig_context);
         } else {
             ConstQuestionPtr question = *message.beginQuestion();
-            const RRType &qtype = question->getType();
+            const RRType& qtype = question->getType();
             if (qtype == RRType::AXFR()) {
                 send_answer = impl_->processXfrQuery(io_message, message,
                                                      buffer, tsig_context);
@@ -585,10 +715,12 @@ AuthSrvImpl::processNormalQuery(const IOMessage& io_message, Message& message,
         // If a memory data source is configured call the separate
         // Query::process()
         const ConstQuestionPtr question = *message.beginQuestion();
-        if (memory_client_ && memory_client_class_ == question->getClass()) {
+        if (memory_client_container_ &&
+            memory_client_class_ == question->getClass()) {
             const RRType& qtype = question->getType();
             const Name& qname = question->getName();
-            query_.process(*memory_client_, qname, qtype, message, dnssec_ok);
+            query_.process(memory_client_container_->getInstance(),
+                           qname, qtype, message, dnssec_ok);
         } else {
             datasrc::Query query(message, cache_, dnssec_ok);
             data_sources_.doQuery(query);
@@ -740,6 +872,15 @@ AuthSrvImpl::processNotify(const IOMessage& io_message, Message& message,
     return (true);
 }
 
+bool
+AuthSrvImpl::processUpdate(const IOMessage& io_message) {
+    // Push the update request to a separate process via the forwarder.
+    // On successful push, the request shouldn't be responded from b10-auth,
+    // so we return false.
+    ddns_forwarder_.push(io_message);
+    return (false);
+}
+
 void
 AuthSrvImpl::incCounter(const int protocol) {
     // Increment query counter.

+ 46 - 24
src/bin/auth/auth_srv.h

@@ -17,12 +17,9 @@
 
 #include <string>
 
-// For InMemoryClientPtr below.  This should be a temporary definition until
-// we reorganize the data source framework.
-#include <boost/shared_ptr.hpp>
-
 #include <cc/data.h>
 #include <config/ccsession.h>
+#include <datasrc/factory.h>
 #include <dns/message.h>
 #include <dns/opcode.h>
 #include <util/buffer.h>
@@ -40,6 +37,11 @@
 #include <auth/statistics.h>
 
 namespace isc {
+namespace util {
+namespace io {
+class BaseSocketSessionForwarder;
+}
+}
 namespace datasrc {
 class InMemoryClient;
 }
@@ -96,7 +98,8 @@ public:
     /// but can refer to a local mock object for testing (or other
     /// experimental) purposes.
     AuthSrv(const bool use_cache,
-            isc::xfr::AbstractXfroutClient& xfrout_client);
+            isc::xfr::AbstractXfroutClient& xfrout_client,
+            isc::util::io::BaseSocketSessionForwarder& ddns_forwarder);
     ~AuthSrv();
     //@}
 
@@ -235,19 +238,14 @@ public:
     ///
     void setXfrinSession(isc::cc::AbstractSession* xfrin_session);
 
-    /// A shared pointer type for \c InMemoryClient.
-    ///
-    /// This is defined inside the \c AuthSrv class as it's supposed to be
-    /// a short term interface until we integrate the in-memory and other
-    /// data source frameworks.
-    typedef boost::shared_ptr<isc::datasrc::InMemoryClient> InMemoryClientPtr;
-
-    /// An immutable shared pointer type for \c InMemoryClient.
-    typedef boost::shared_ptr<const isc::datasrc::InMemoryClient>
-    ConstInMemoryClientPtr;
-
     /// Returns the in-memory data source configured for the \c AuthSrv,
-    /// if any.
+    /// if any, as a pointer.
+    ///
+    /// This is mostly a convenience function around
+    /// \c getInMemoryClientContainer, which saves the caller the step
+    /// of having to call getInstance().
+    /// The pointer is of course only valid as long as the container
+    /// exists.
     ///
     /// The in-memory data source is configured per RR class.  However,
     /// the data source may not be available for all RR classes.
@@ -262,24 +260,48 @@ public:
     /// \param rrclass The RR class of the requested in-memory data source.
     /// \return A pointer to the in-memory data source, if configured;
     /// otherwise NULL.
-    InMemoryClientPtr getInMemoryClient(const isc::dns::RRClass& rrclass);
+    isc::datasrc::DataSourceClient* getInMemoryClient(
+        const isc::dns::RRClass& rrclass);
+
+    /// Returns the DataSourceClientContainer of the in-memory datasource
+    ///
+    /// \exception InvalidParameter if the given class does not match
+    ///            the one in the memory data source, or if the memory
+    ///            datasource has not been set (callers can check with
+    ///            \c hasMemoryDataSource())
+    ///
+    /// \param rrclass The RR class of the requested in-memory data source.
+    /// \return A shared pointer to the in-memory data source, if configured;
+    /// otherwise an empty shared pointer.
+    isc::datasrc::DataSourceClientContainerPtr getInMemoryClientContainer(
+        const isc::dns::RRClass& rrclass);
+
+    /// Checks if the in-memory data source has been set.
+    ///
+    /// Right now, only one datasource at a time is effectively supported.
+    /// This is a helper method to check whether it is the in-memory one.
+    /// This is mostly useful for current testing, and is expected to be
+    /// removed (or changed in behaviour) soon, when the general
+    /// multi-data-source framework is completed.
+    ///
+    /// \return True if the in-memory datasource has been set.
+    bool hasInMemoryClient() const;
 
     /// Sets or replaces the in-memory data source of the specified RR class.
     ///
-    /// As noted in \c getInMemoryClient(), some RR classes may not be
-    /// supported, in which case an exception of class \c InvalidParameter
-    /// will be thrown.
+    /// Some RR classes may not be supported, in which case an exception
+    /// of class \c InvalidParameter will be thrown.
     /// This method never throws an exception otherwise.
     ///
     /// If there is already an in memory data source configured, it will be
     /// replaced with the newly specified one.
-    /// \c memory_datasrc can be NULL, in which case it will (re)disable the
-    /// in-memory data source.
+    /// \c memory_client can be an empty shared pointer, in which case it
+    /// will (re)disable the in-memory data source.
     ///
     /// \param rrclass The RR class of the in-memory data source to be set.
     /// \param memory_client A (shared) pointer to \c InMemoryClient to be set.
     void setInMemoryClient(const isc::dns::RRClass& rrclass,
-                           InMemoryClientPtr memory_client);
+        isc::datasrc::DataSourceClientContainerPtr memory_client);
 
     /// \brief Set the communication session with Statistics.
     ///

+ 18 - 6
src/bin/auth/b10-auth.8

@@ -2,12 +2,12 @@
 .\"     Title: b10-auth
 .\"    Author: [FIXME: author] [see http://docbook.sf.net/el/author]
 .\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
-.\"      Date: March 28, 2012
+.\"      Date: June 20, 2012
 .\"    Manual: BIND10
 .\"    Source: BIND10
 .\"  Language: English
 .\"
-.TH "B10\-AUTH" "8" "March 28, 2012" "BIND10" "BIND10"
+.TH "B10\-AUTH" "8" "June 20, 2012" "BIND10" "BIND10"
 .\" -----------------------------------------------------------------
 .\" * set default formatting
 .\" -----------------------------------------------------------------
@@ -49,7 +49,7 @@ Do not cache answers in memory\&. The default is to use the cache for faster res
 .PP
 \fB\-v\fR
 .RS 4
-Enabled verbose mode\&. This enables diagnostic messages to STDERR\&.
+Enable verbose logging mode\&. This enables logging of diagnostic messages at the maximum debug level\&.
 .RE
 .SH "CONFIGURATION AND COMMANDS"
 .PP
@@ -72,9 +72,21 @@ to optionally select the class (it defaults to
 \fIzones\fR
 to define the
 \fIfile\fR
-path name and the
+path name,
 \fIorigin\fR
-(default domain)\&. By default, this is empty\&.
+(default domain), and optional
+\fIfiletype\fR\&. By default,
+\fIzones\fR
+is empty\&. For the in\-memory data source (i\&.e\&., the
+\fItype\fR
+is
+\(lqmemory\(rq), the optional
+\fIfiletype\fR
+configuration item for
+\fIzones\fR
+can be specified so the in\-memory zone data can be built from another data source that is based on a database backend (in practice with current implementation, it would be an SQLite3 database file for the SQLite3 data source)\&. See the
+BIND 10 Guide
+for configuration details\&.
 .if n \{\
 .sp
 .\}
@@ -88,7 +100,7 @@ path name and the
 .ps -1
 .br
 .sp
-In this development version, currently this is only used for the memory data source\&. Only the IN class is supported at this time\&. By default, the memory data source is disabled\&. Also, currently the zone file must be canonical such as generated by \fBnamed\-compilezone \-D\fR\&.
+Only the IN class is supported at this time\&. By default, the memory data source is disabled\&. Also, currently the zone file must be canonical such as generated by \fBnamed\-compilezone \-D\fR\&.
 .sp .5v
 .RE
 .PP

+ 16 - 9
src/bin/auth/b10-auth.xml

@@ -20,7 +20,7 @@
 <refentry>
 
   <refentryinfo>
-    <date>March 28, 2012</date>
+    <date>June 20, 2012</date>
   </refentryinfo>
 
   <refmeta>
@@ -94,8 +94,8 @@
       <varlistentry>
         <term><option>-v</option></term>
         <listitem><para>
-          Enabled verbose mode. This enables diagnostic messages to
-          STDERR.
+	  Enable verbose logging mode. This enables logging of
+	  diagnostic messages at the maximum debug level.
         </para></listitem>
       </varlistentry>
 
@@ -125,14 +125,21 @@
       (it defaults to <quote>IN</quote>);
       and
       <varname>zones</varname> to define the
-      <varname>file</varname> path name and the
-      <varname>origin</varname> (default domain).
-
-      By default, this is empty.
+      <varname>file</varname> path name,
+      <varname>origin</varname> (default domain), and optional
+      <varname>filetype</varname>.
+      By default, <varname>zones</varname> is empty.
+      For the in-memory data source (i.e., the <varname>type</varname>
+      is <quote>memory</quote>), the optional <varname>filetype</varname>
+      configuration item for <varname>zones</varname> can be
+      specified so the in-memory zone data can be built from another
+      data source that is based on a database backend (in practice
+      with current implementation, it would be an SQLite3 database
+      file for the SQLite3 data source).
+      See the <citetitle>BIND 10 Guide</citetitle> for configuration
+      details.
 
       <note><simpara>
-        In this development version, currently this is only used for the
-        memory data source.
         Only the IN class is supported at this time.
         By default, the memory data source is disabled.
         Also, currently the zone file must be canonical such as

+ 6 - 2
src/bin/auth/benchmarks/query_bench.cc

@@ -31,9 +31,10 @@
 #include <dns/rrclass.h>
 
 #include <log/logger_support.h>
-
 #include <xfr/xfrout_client.h>
 
+#include <util/unittests/mock_socketsession.h>
+
 #include <auth/auth_srv.h>
 #include <auth/auth_config.h>
 #include <auth/query.h>
@@ -48,6 +49,7 @@ using namespace isc::auth;
 using namespace isc::dns;
 using namespace isc::log;
 using namespace isc::util;
+using namespace isc::util::unittests;
 using namespace isc::xfr;
 using namespace isc::bench;
 using namespace isc::asiodns;
@@ -78,7 +80,7 @@ protected:
     QueryBenchMark(const bool enable_cache,
                    const BenchQueries& queries, Message& query_message,
                    OutputBuffer& buffer) :
-        server_(new AuthSrv(enable_cache, xfrout_client)),
+        server_(new AuthSrv(enable_cache, xfrout_client, ddns_forwarder)),
         queries_(queries),
         query_message_(query_message),
         buffer_(buffer),
@@ -103,6 +105,8 @@ public:
 
         return (queries_.size());
     }
+private:
+    MockSocketSessionForwarder ddns_forwarder;
 protected:
     AuthSrvPtr server_;
 private:

+ 7 - 4
src/bin/auth/command.cc

@@ -210,8 +210,8 @@ private:
         const RRClass zone_class =
             class_elem ? RRClass(class_elem->stringValue()) : RRClass::IN();
 
-        AuthSrv::InMemoryClientPtr datasrc(server.
-                                           getInMemoryClient(zone_class));
+        isc::datasrc::DataSourceClient* datasrc(
+            server.getInMemoryClient(zone_class));
         if (datasrc == NULL) {
             isc_throw(AuthCommandError, "Memory data source is disabled");
         }
@@ -223,13 +223,16 @@ private:
         const Name origin = Name(origin_elem->stringValue());
 
         // Get the current zone
-        const InMemoryClient::FindResult result = datasrc->findZone(origin);
+        const DataSourceClient::FindResult result = datasrc->findZone(origin);
         if (result.code != result::SUCCESS) {
             isc_throw(AuthCommandError, "Zone " << origin <<
                       " is not found in data source");
         }
 
-        old_zone_finder_ = boost::dynamic_pointer_cast<InMemoryZoneFinder>(
+        // It would appear that dynamic_cast does not work on all systems;
+        // it seems to confuse the RTTI system, resulting in NULL return
+        // values. So we use the more dangerous static_pointer_cast here.
+        old_zone_finder_ = boost::static_pointer_cast<InMemoryZoneFinder>(
             result.zone_finder);
 
         return (true);

+ 19 - 1
src/bin/auth/common.cc

@@ -33,7 +33,25 @@ getXfroutSocketPath() {
         if (getenv("BIND10_XFROUT_SOCKET_FILE") != NULL) {
             return (getenv("BIND10_XFROUT_SOCKET_FILE"));
         } else {
-            return (UNIX_SOCKET_FILE);
+            return (UNIX_XFROUT_SOCKET_FILE);
+        }
+    }
+}
+
+string
+getDDNSSocketPath() {
+    if (getenv("B10_FROM_BUILD") != NULL) {
+        if (getenv("B10_FROM_SOURCE_LOCALSTATEDIR") != NULL) {
+            return (string(getenv("B10_FROM_SOURCE_LOCALSTATEDIR")) +
+                    "/ddns_socket");
+        } else {
+            return (string(getenv("B10_FROM_BUILD")) + "/ddns_socket");
+        }
+    } else {
+        if (getenv("BIND10_DDNS_SOCKET_FILE") != NULL) {
+            return (getenv("BIND10_DDNS_SOCKET_FILE"));
+        } else {
+            return (UNIX_DDNS_SOCKET_FILE);
         }
     }
 }

+ 14 - 0
src/bin/auth/common.h

@@ -38,6 +38,20 @@ public:
 /// The logic should be the same as in b10-xfrout, so they find each other.
 std::string getXfroutSocketPath();
 
+/// \brief Get the path of socket to talk to ddns
+///
+/// It takes some environment variables into account (B10_FROM_BUILD,
+/// B10_FROM_SOURCE_LOCALSTATEDIR and BIND10_DDNS_SOCKET_FILE). It
+/// also considers the installation prefix.
+///
+/// The logic should be the same as in b10-ddns, so they find each other.
+///
+/// Note: eventually we should find a better way so that we don't have to
+/// repeat the same magic value (and how to tweak it with some magic
+/// environment variable) twice, at which point this function may be able
+/// to be deprecated.
+std::string getDDNSSocketPath();
+
 /// \brief The name used when identifieng the process
 ///
 /// This is currently b10-auth, but it can be changed easily in one place.

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

@@ -28,6 +28,7 @@
 #include <exceptions/exceptions.h>
 
 #include <util/buffer.h>
+#include <util/io/socketsession.h>
 
 #include <dns/message.h>
 #include <dns/messagerenderer.h>
@@ -60,6 +61,7 @@ using namespace isc::data;
 using namespace isc::dns;
 using namespace isc::log;
 using namespace isc::util;
+using namespace isc::util::io;
 using namespace isc::xfr;
 
 namespace {
@@ -85,7 +87,7 @@ usage() {
     cerr << "Usage:  b10-auth [-u user] [-nv]"
          << endl;
     cerr << "\t-n: do not cache answers in memory" << endl;
-    cerr << "\t-v: verbose output" << endl;
+    cerr << "\t-v: verbose logging (debug-level)" << endl;
     exit(1);
 }
 
@@ -130,6 +132,7 @@ main(int argc, char* argv[]) {
     bool statistics_session_established = false; // XXX (see Trac #287)
     ModuleCCSession* config_session = NULL;
     XfroutClient xfrout_client(getXfroutSocketPath());
+    SocketSessionForwarder ddns_forwarder(getDDNSSocketPath());
     try {
         string specfile;
         if (getenv("B10_FROM_BUILD")) {
@@ -139,7 +142,7 @@ main(int argc, char* argv[]) {
             specfile = string(AUTH_SPECFILE_LOCATION);
         }
 
-        auth_server = new AuthSrv(cache, xfrout_client);
+        auth_server = new AuthSrv(cache, xfrout_client, ddns_forwarder);
         LOG_INFO(auth_logger, AUTH_SERVER_CREATED);
 
         SimpleCallback* checkin = auth_server->getCheckinProvider();

+ 2 - 1
src/bin/auth/spec_config.h.pre.in

@@ -13,4 +13,5 @@
 // PERFORMANCE OF THIS SOFTWARE.
 
 #define AUTH_SPECFILE_LOCATION "@prefix@/share/@PACKAGE@/auth.spec"
-#define UNIX_SOCKET_FILE "@@LOCALSTATEDIR@@/@PACKAGE@/auth_xfrout_conn"
+#define UNIX_XFROUT_SOCKET_FILE "@@LOCALSTATEDIR@@/@PACKAGE@/auth_xfrout_conn"
+#define UNIX_DDNS_SOCKET_FILE "@@LOCALSTATEDIR@@/@PACKAGE@/ddns_socket"

+ 9 - 6
src/bin/auth/tests/Makefile.am

@@ -19,6 +19,9 @@ endif
 
 CLEANFILES = *.gcno *.gcda
 
+TESTS_ENVIRONMENT = \
+        libtool --mode=execute $(VALGRIND_COMMAND)
+
 # Do not define global tests, use check-local so
 # environment can be set (needed for dynamic loading)
 TESTS =
@@ -44,9 +47,9 @@ run_unittests_SOURCES += statistics_unittest.cc
 run_unittests_SOURCES += run_unittests.cc
 # This is a temporary workaround for #1206, where the InMemoryClient has been
 # moved to an ldopened library. We could add that library to LDADD, but that
-# is nonportable. When #1207 is done this becomes moot anyway, and the
-# specific workaround is not needed anymore, so we can then remove this
-# line again.
+# is nonportable. This should've been moot after #1207, but there is still
+# one dependency; the in-memory-specific zone loader call is still in
+# auth.
 run_unittests_SOURCES += ${top_srcdir}/src/lib/datasrc/memory_datasrc.cc
 
 
@@ -54,9 +57,7 @@ nodist_run_unittests_SOURCES = ../auth_messages.h ../auth_messages.cc
 
 run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
-run_unittests_LDADD = $(GTEST_LDADD)
-run_unittests_LDADD += $(SQLITE_LIBS)
-run_unittests_LDADD += $(top_builddir)/src/lib/testutils/libtestutils.la
+run_unittests_LDADD = $(top_builddir)/src/lib/testutils/libtestutils.la
 run_unittests_LDADD +=  $(top_builddir)/src/lib/datasrc/libdatasrc.la
 run_unittests_LDADD +=  $(top_builddir)/src/lib/dns/libdns++.la
 run_unittests_LDADD += $(top_builddir)/src/lib/util/libutil.la
@@ -71,6 +72,8 @@ run_unittests_LDADD += $(top_builddir)/src/lib/server_common/libserver_common.la
 run_unittests_LDADD += $(top_builddir)/src/lib/nsas/libnsas.la
 run_unittests_LDADD += $(top_builddir)/src/lib/util/unittests/libutil_unittests.la
 run_unittests_LDADD += $(top_builddir)/src/lib/statistics/libstatistics.la
+run_unittests_LDADD += $(GTEST_LDADD)
+run_unittests_LDADD += $(SQLITE_LIBS)
 
 check-local:
 	B10_FROM_BUILD=${abs_top_builddir} ./run_unittests

+ 305 - 51
src/bin/auth/tests/auth_srv_unittest.cc

@@ -14,11 +14,7 @@
 
 #include <config.h>
 
-#include <vector>
-
-#include <boost/shared_ptr.hpp>
-
-#include <gtest/gtest.h>
+#include <util/io/sockaddr_util.h>
 
 #include <dns/message.h>
 #include <dns/messagerenderer.h>
@@ -38,6 +34,7 @@
 #include <auth/common.h>
 #include <auth/statistics.h>
 
+#include <util/unittests/mock_socketsession.h>
 #include <dns/tests/unittest_util.h>
 #include <testutils/dnsmessage_test.h>
 #include <testutils/srv_test.h>
@@ -45,10 +42,24 @@
 #include <testutils/portconfig.h>
 #include <testutils/socket_request.h>
 
+#include <gtest/gtest.h>
+
+#include <boost/lexical_cast.hpp>
+#include <boost/shared_ptr.hpp>
+#include <boost/scoped_ptr.hpp>
+
+#include <vector>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+
 using namespace std;
 using namespace isc::cc;
 using namespace isc::dns;
 using namespace isc::util;
+using namespace isc::util::io::internal;
+using namespace isc::util::unittests;
 using namespace isc::dns::rdata;
 using namespace isc::data;
 using namespace isc::xfr;
@@ -57,6 +68,7 @@ using namespace isc::asiolink;
 using namespace isc::testutils;
 using namespace isc::server_common::portconfig;
 using isc::UnitTestUtil;
+using boost::scoped_ptr;
 
 namespace {
 const char* const CONFIG_TESTDB =
@@ -77,7 +89,7 @@ class AuthSrvTest : public SrvTestBase {
 protected:
     AuthSrvTest() :
         dnss_(),
-        server(true, xfrout),
+        server(true, xfrout, ddns_forwarder),
         rrclass(RRClass::IN()),
         // The empty string is expected value of the parameter of
         // requestSocket, not the app_name (there's no fallback, it checks
@@ -89,6 +101,13 @@ protected:
         server.setStatisticsSession(&statistics_session);
     }
 
+    ~AuthSrvTest() {
+        // Clear the message now; depending on the RTTI implementation,
+        // type information may be lost if the message is cleared
+        // automatically later, so as a precaution we do it now.
+        parse_message->clear(Message::PARSE);
+    }
+
     virtual void processMessage() {
         // If processMessage has been called before, parse_message needs
         // to be reset. If it hasn't, there's no harm in doing so
@@ -136,9 +155,30 @@ protected:
                     opcode.getCode(), QR_FLAG, 1, 0, 0, 0);
     }
 
+    // Convenient shortcut of creating a simple request and having the
+    // server process it.
+    void createAndSendRequest(RRType req_type, Opcode opcode = Opcode::QUERY(),
+                              const Name& req_name = Name("example.com"),
+                              RRClass req_class = RRClass::IN(),
+                              int protocol = IPPROTO_UDP,
+                              const char* const remote_address =
+                              DEFAULT_REMOTE_ADDRESS,
+                              uint16_t remote_port = DEFAULT_REMOTE_PORT)
+    {
+        UnitTestUtil::createRequestMessage(request_message, opcode,
+                                           default_qid, req_name,
+                                           req_class, req_type);
+        createRequestPacket(request_message, protocol, NULL,
+                            remote_address, remote_port);
+        parse_message->clear(Message::PARSE);
+        server.processMessage(*io_message, *parse_message, *response_obuffer,
+                              &dnsserv);
+    }
+
     MockDNSService dnss_;
     MockSession statistics_session;
     MockXfroutClient xfrout;
+    MockSocketSessionForwarder ddns_forwarder;
     AuthSrv server;
     const RRClass rrclass;
     vector<uint8_t> response_data;
@@ -246,8 +286,8 @@ TEST_F(AuthSrvTest, iqueryViaDNSServer) {
 // Unsupported requests.  Should result in NOTIMP.
 TEST_F(AuthSrvTest, unsupportedRequest) {
     unsupportedRequest();
-    // unsupportedRequest tries 14 different opcodes
-    checkAllRcodeCountersZeroExcept(Rcode::NOTIMP(), 14);
+    // unsupportedRequest tries 13 different opcodes
+    checkAllRcodeCountersZeroExcept(Rcode::NOTIMP(), 13);
 }
 
 // Multiple questions.  Should result in FORMERR.
@@ -830,16 +870,23 @@ TEST_F(AuthSrvTest, updateConfigFail) {
                 QR_FLAG | AA_FLAG, 1, 1, 1, 0);
 }
 
-TEST_F(AuthSrvTest, updateWithInMemoryClient) {
+TEST_F(AuthSrvTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_updateWithInMemoryClient
+#else
+       updateWithInMemoryClient
+#endif
+    )
+{
     // Test configuring memory data source.  Detailed test cases are covered
     // in the configuration tests.  We only check the AuthSrv interface here.
 
     // By default memory data source isn't enabled
-    EXPECT_EQ(AuthSrv::InMemoryClientPtr(), server.getInMemoryClient(rrclass));
+    EXPECT_FALSE(server.hasInMemoryClient());
     updateConfig(&server,
                  "{\"datasources\": [{\"type\": \"memory\"}]}", true);
     // after successful configuration, we should have one (with empty zoneset).
-    ASSERT_NE(AuthSrv::InMemoryClientPtr(), server.getInMemoryClient(rrclass));
+    EXPECT_TRUE(server.hasInMemoryClient());
     EXPECT_EQ(0, server.getInMemoryClient(rrclass)->getZoneCount());
 
     // The memory data source is empty, should return REFUSED rcode.
@@ -851,13 +898,20 @@ TEST_F(AuthSrvTest, updateWithInMemoryClient) {
                 opcode.getCode(), QR_FLAG, 1, 0, 0, 0);
 }
 
-TEST_F(AuthSrvTest, queryWithInMemoryClientNoDNSSEC) {
+TEST_F(AuthSrvTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_queryWithInMemoryClientNoDNSSEC
+#else
+       queryWithInMemoryClientNoDNSSEC
+#endif
+    )
+{
     // In this example, we do simple check that query is handled from the
     // query handler class, and confirm it returns no error and a non empty
     // answer section.  Detailed examination on the response content
     // for various types of queries are tested in the query tests.
     updateConfig(&server, CONFIG_INMEMORY_EXAMPLE, true);
-    ASSERT_NE(AuthSrv::InMemoryClientPtr(), server.getInMemoryClient(rrclass));
+    EXPECT_TRUE(server.hasInMemoryClient());
     EXPECT_EQ(1, server.getInMemoryClient(rrclass)->getZoneCount());
 
     createDataFromFile("nsec3query_nodnssec_fromWire.wire");
@@ -869,12 +923,19 @@ TEST_F(AuthSrvTest, queryWithInMemoryClientNoDNSSEC) {
                 opcode.getCode(), QR_FLAG | AA_FLAG, 1, 1, 2, 1);
 }
 
-TEST_F(AuthSrvTest, queryWithInMemoryClientDNSSEC) {
+TEST_F(AuthSrvTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_queryWithInMemoryClientDNSSEC
+#else
+       queryWithInMemoryClientDNSSEC
+#endif
+    )
+{
     // Similar to the previous test, but the query has the DO bit on.
     // The response should contain RRSIGs, and should have more RRs than
     // the previous case.
     updateConfig(&server, CONFIG_INMEMORY_EXAMPLE, true);
-    ASSERT_NE(AuthSrv::InMemoryClientPtr(), server.getInMemoryClient(rrclass));
+    EXPECT_TRUE(server.hasInMemoryClient());
     EXPECT_EQ(1, server.getInMemoryClient(rrclass)->getZoneCount());
 
     createDataFromFile("nsec3query_fromWire.wire");
@@ -886,7 +947,14 @@ TEST_F(AuthSrvTest, queryWithInMemoryClientDNSSEC) {
                 opcode.getCode(), QR_FLAG | AA_FLAG, 1, 2, 3, 3);
 }
 
-TEST_F(AuthSrvTest, chQueryWithInMemoryClient) {
+TEST_F(AuthSrvTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_chQueryWithInMemoryClient
+#else
+       chQueryWithInMemoryClient
+#endif
+    )
+{
     // Configure memory data source for class IN
     updateConfig(&server, "{\"datasources\": "
                  "[{\"class\": \"IN\", \"type\": \"memory\"}]}", true);
@@ -1108,7 +1176,7 @@ TEST_F(AuthSrvTest, processNormalQuery_reuseRenderer2) {
 //
 namespace {
 
-/// A the possible methods to throw in, either in FakeInMemoryClient or
+/// The possible methods to throw in, either in FakeClient or
 /// FakeZoneFinder
 enum ThrowWhen {
     THROW_NEVER,
@@ -1132,10 +1200,10 @@ checkThrow(ThrowWhen method, ThrowWhen throw_at, bool isc_exception) {
     }
 }
 
-/// \brief proxy class for the ZoneFinder returned by the InMemoryClient
-///        proxied by FakeInMemoryClient
+/// \brief proxy class for the ZoneFinder returned by the Client
+///        proxied by FakeClient
 ///
-/// See the documentation for FakeInMemoryClient for more information,
+/// See the documentation for FakeClient for more information,
 /// all methods simply check whether they should throw, and if not, call
 /// their proxied equivalent.
 class FakeZoneFinder : public isc::datasrc::ZoneFinder {
@@ -1196,11 +1264,6 @@ public:
         return (real_zone_finder_->findNSEC3(name, recursive));
     }
 
-    virtual isc::dns::Name
-    findPreviousName(const isc::dns::Name& query) const {
-        return (real_zone_finder_->findPreviousName(query));
-    }
-
 private:
     isc::datasrc::ZoneFinderPtr real_zone_finder_;
     ThrowWhen throw_when_;
@@ -1208,15 +1271,15 @@ private:
     ConstRRsetPtr fake_rrset_;
 };
 
-/// \brief Proxy InMemoryClient that can throw exceptions at specified times
+/// \brief Proxy FakeClient that can throw exceptions at specified times
 ///
-/// It is based on the memory client since that one is easy to override
-/// (with setInMemoryClient) with the current design of AuthSrv.
-class FakeInMemoryClient : public isc::datasrc::InMemoryClient {
+/// Currently it is used as an 'InMemoryClient' using setInMemoryClient,
+/// but it is in effect a general datasource client.
+class FakeClient : public isc::datasrc::DataSourceClient {
 public:
     /// \brief Create a proxy memory client
     ///
-    /// \param real_client The real in-memory client to proxy
+    /// \param real_client The real (in-memory) client to proxy
     /// \param throw_when if set to any value other than never, that is
     ///        the method that will throw an exception (either in this
     ///        class or the related FakeZoneFinder)
@@ -1224,10 +1287,10 @@ public:
     ///                      throw std::exception
     /// \param fake_rrset If non NULL, it will be used as an answer to
     /// find() for that name and type.
-    FakeInMemoryClient(AuthSrv::InMemoryClientPtr real_client,
-                       ThrowWhen throw_when, bool isc_exception,
-                       ConstRRsetPtr fake_rrset = ConstRRsetPtr()) :
-        real_client_(real_client),
+    FakeClient(isc::datasrc::DataSourceClientContainerPtr real_client,
+               ThrowWhen throw_when, bool isc_exception,
+               ConstRRsetPtr fake_rrset = ConstRRsetPtr()) :
+        real_client_ptr_(real_client),
         throw_when_(throw_when),
         isc_exception_(isc_exception),
         fake_rrset_(fake_rrset)
@@ -1242,7 +1305,8 @@ public:
     virtual FindResult
     findZone(const isc::dns::Name& name) const {
         checkThrow(THROW_AT_FIND_ZONE, throw_when_, isc_exception_);
-        const FindResult result = real_client_->findZone(name);
+        const FindResult result =
+            real_client_ptr_->getInstance().findZone(name);
         return (FindResult(result.code, isc::datasrc::ZoneFinderPtr(
                                         new FakeZoneFinder(result.zone_finder,
                                                            throw_when_,
@@ -1250,28 +1314,74 @@ public:
                                                            fake_rrset_))));
     }
 
+    isc::datasrc::ZoneUpdaterPtr
+    getUpdater(const isc::dns::Name&, bool, bool) const {
+        isc_throw(isc::NotImplemented,
+                  "Update attempt on in fake data source");
+    }
+    std::pair<isc::datasrc::ZoneJournalReader::Result,
+              isc::datasrc::ZoneJournalReaderPtr>
+    getJournalReader(const isc::dns::Name&, uint32_t, uint32_t) const {
+        isc_throw(isc::NotImplemented, "Journaling isn't supported for "
+                  "fake data source");
+    }
 private:
-    AuthSrv::InMemoryClientPtr real_client_;
+    const isc::datasrc::DataSourceClientContainerPtr real_client_ptr_;
     ThrowWhen throw_when_;
     bool isc_exception_;
     ConstRRsetPtr fake_rrset_;
 };
 
+class FakeContainer : public isc::datasrc::DataSourceClientContainer {
+public:
+    /// \brief Creates a fake container for the given in-memory client
+    ///
+    /// The initializer creates a fresh instance of a memory datasource,
+    /// which is ignored for the rest (but we do not allow 'null' containers
+    /// atm, and this is only needed in these tests, this may be changed
+    /// if we generalize the container class a bit more)
+    ///
+    /// It will also create a FakeClient, with the given arguments, which
+    /// is actually used when the instance is requested.
+    FakeContainer(isc::datasrc::DataSourceClientContainerPtr real_client,
+                  ThrowWhen throw_when, bool isc_exception,
+                  ConstRRsetPtr fake_rrset = ConstRRsetPtr()) :
+        DataSourceClientContainer("memory",
+                                  Element::fromJSON("{\"type\": \"memory\"}")),
+        client_(new FakeClient(real_client, throw_when, isc_exception,
+                               fake_rrset))
+    {}
+
+    isc::datasrc::DataSourceClient& getInstance() {
+        return (*client_);
+    }
+
+private:
+    const boost::scoped_ptr<isc::datasrc::DataSourceClient> client_;
+};
+
 } // end anonymous namespace for throwing proxy classes
 
 // Test for the tests
 //
 // Set the proxies to never throw, this should have the same result as
 // queryWithInMemoryClientNoDNSSEC, and serves to test the two proxy classes
-TEST_F(AuthSrvTest, queryWithInMemoryClientProxy) {
+TEST_F(AuthSrvTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_queryWithInMemoryClientProxy
+#else
+       queryWithInMemoryClientProxy
+#endif
+    )
+{
     // Set real inmem client to proxy
     updateConfig(&server, CONFIG_INMEMORY_EXAMPLE, true);
+    EXPECT_TRUE(server.hasInMemoryClient());
 
-    AuthSrv::InMemoryClientPtr fake_client(
-        new FakeInMemoryClient(server.getInMemoryClient(rrclass),
-                               THROW_NEVER, false));
-    ASSERT_NE(AuthSrv::InMemoryClientPtr(), server.getInMemoryClient(rrclass));
-    server.setInMemoryClient(rrclass, fake_client);
+    isc::datasrc::DataSourceClientContainerPtr fake_client_container(
+        new FakeContainer(server.getInMemoryClientContainer(rrclass),
+                          THROW_NEVER, false));
+    server.setInMemoryClient(rrclass, fake_client_container);
 
     createDataFromFile("nsec3query_nodnssec_fromWire.wire");
     server.processMessage(*io_message, *parse_message, *response_obuffer,
@@ -1297,17 +1407,23 @@ setupThrow(AuthSrv* server, const char *config, ThrowWhen throw_when,
 
     // Set it to throw on findZone(), this should result in
     // SERVFAIL on any exception
-    AuthSrv::InMemoryClientPtr fake_client(
-        new FakeInMemoryClient(
-            server->getInMemoryClient(isc::dns::RRClass::IN()),
+    isc::datasrc::DataSourceClientContainerPtr fake_client_container(
+        new FakeContainer(
+            server->getInMemoryClientContainer(isc::dns::RRClass::IN()),
             throw_when, isc_exception, rrset));
 
-    ASSERT_NE(AuthSrv::InMemoryClientPtr(),
-              server->getInMemoryClient(isc::dns::RRClass::IN()));
-    server->setInMemoryClient(isc::dns::RRClass::IN(), fake_client);
+    ASSERT_TRUE(server->hasInMemoryClient());
+    server->setInMemoryClient(isc::dns::RRClass::IN(), fake_client_container);
 }
 
-TEST_F(AuthSrvTest, queryWithThrowingProxyServfails) {
+TEST_F(AuthSrvTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_queryWithThrowingProxyServfails
+#else
+       queryWithThrowingProxyServfails
+#endif
+    )
+{
     // Test the common cases, all of which should simply return SERVFAIL
     // Use THROW_NEVER as end marker
     ThrowWhen throws[] = { THROW_AT_FIND_ZONE,
@@ -1331,7 +1447,14 @@ TEST_F(AuthSrvTest, queryWithThrowingProxyServfails) {
 
 // Throw isc::Exception in getClass(). (Currently?) getClass is not called
 // in the processMessage path, so this should result in a normal answer
-TEST_F(AuthSrvTest, queryWithInMemoryClientProxyGetClass) {
+TEST_F(AuthSrvTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_queryWithInMemoryClientProxyGetClass
+#else
+       queryWithInMemoryClientProxyGetClass
+#endif
+    )
+{
     createDataFromFile("nsec3query_nodnssec_fromWire.wire");
     setupThrow(&server, CONFIG_INMEMORY_EXAMPLE, THROW_AT_GET_CLASS, true);
 
@@ -1344,7 +1467,14 @@ TEST_F(AuthSrvTest, queryWithInMemoryClientProxyGetClass) {
                 opcode.getCode(), QR_FLAG | AA_FLAG, 1, 1, 2, 1);
 }
 
-TEST_F(AuthSrvTest, queryWithThrowingInToWire) {
+TEST_F(AuthSrvTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_queryWithThrowingInToWire
+#else
+       queryWithThrowingInToWire
+#endif
+    )
+{
     // Set up a faked data source.  It will return an empty RRset for the
     // query.
     ConstRRsetPtr empty_rrset(new RRset(Name("foo.example"),
@@ -1385,4 +1515,128 @@ TEST_F(AuthSrvTest, queryWithThrowingInToWire) {
                 opcode.getCode(), QR_FLAG, 1, 0, 0, 0);
 }
 
+//
+// DDNS related tests
+//
+
+// Helper subroutine to check if the given socket address has the expected
+// address and port.  It depends on specific output of getnameinfo() (while
+// there can be multiple textual representation of the same address) but
+// in practice it should be reliable.
+void
+checkAddrPort(const struct sockaddr& actual_sa,
+              const string& expected_addr, uint16_t expected_port)
+{
+    char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
+    const int error = getnameinfo(&actual_sa, getSALength(actual_sa), hbuf,
+                                  sizeof(hbuf), sbuf, sizeof(sbuf),
+                                  NI_NUMERICHOST | NI_NUMERICSERV);
+    if (error != 0) {
+        isc_throw(isc::Unexpected, "getnameinfo failed: " <<
+                  gai_strerror(error));
+    }
+    EXPECT_EQ(expected_addr, hbuf);
+    EXPECT_EQ(boost::lexical_cast<string>(expected_port), sbuf);
+}
+
+TEST_F(AuthSrvTest, DDNSForward) {
+    EXPECT_FALSE(ddns_forwarder.isConnected());
+
+    // Repeat sending an update request 4 times, differing some network
+    // parameters: UDP/IPv4, TCP/IPv4, UDP/IPv6, TCP/IPv6, in this order.
+    // By doing that we can also confirm the forwarder connection will be
+    // established exactly once, and kept established.
+    for (size_t i = 0; i < 4; ++i) {
+        // Use different names for some different cases
+        const Name zone_name = Name(i < 2 ? "example.com" : "example.org");
+        const socklen_t family = (i < 2) ? AF_INET : AF_INET6;
+        const char* const remote_addr =
+            (family == AF_INET) ? "192.0.2.1" : "2001:db8::1";
+        const uint16_t remote_port =
+            (family == AF_INET) ? 53214 : 53216;
+        const int protocol = ((i % 2) == 0) ? IPPROTO_UDP : IPPROTO_TCP;
+
+        createAndSendRequest(RRType::SOA(), Opcode::UPDATE(), zone_name,
+                             RRClass::IN(), protocol, remote_addr,
+                             remote_port);
+        EXPECT_FALSE(dnsserv.hasAnswer());
+        EXPECT_TRUE(ddns_forwarder.isConnected());
+
+        // Examine the pushed data (note: currently "local end" has a dummy
+        // value equal to remote)
+        EXPECT_EQ(family, ddns_forwarder.getPushedFamily());
+        const int expected_type =
+            (protocol == IPPROTO_UDP) ? SOCK_DGRAM : SOCK_STREAM;
+        EXPECT_EQ(expected_type, ddns_forwarder.getPushedType());
+        EXPECT_EQ(protocol, ddns_forwarder.getPushedProtocol());
+        checkAddrPort(ddns_forwarder.getPushedRemoteend(),
+                      remote_addr, remote_port);
+        checkAddrPort(ddns_forwarder.getPushedLocalend(),
+                      remote_addr, remote_port);
+        EXPECT_EQ(io_message->getDataSize(),
+                  ddns_forwarder.getPushedData().size());
+        EXPECT_EQ(0, memcmp(io_message->getData(),
+                            &ddns_forwarder.getPushedData()[0],
+                            ddns_forwarder.getPushedData().size()));
+    }
+}
+
+TEST_F(AuthSrvTest, DDNSForwardConnectFail) {
+    // make connect attempt fail.  It should result in SERVFAIL.  Note that
+    // the question (zone) section should be cleared for opcode of update.
+    ddns_forwarder.disableConnect();
+    createAndSendRequest(RRType::SOA(), Opcode::UPDATE());
+    EXPECT_TRUE(dnsserv.hasAnswer());
+    headerCheck(*parse_message, default_qid, Rcode::SERVFAIL(),
+                Opcode::UPDATE().getCode(), QR_FLAG, 0, 0, 0, 0);
+    EXPECT_FALSE(ddns_forwarder.isConnected());
+
+    // Now make connect okay again.  Despite the previous failure the new
+    // connection should now be established.
+    ddns_forwarder.enableConnect();
+    createAndSendRequest(RRType::SOA(), Opcode::UPDATE());
+    EXPECT_FALSE(dnsserv.hasAnswer());
+    EXPECT_TRUE(ddns_forwarder.isConnected());
+}
+
+TEST_F(AuthSrvTest, DDNSForwardPushFail) {
+    // Make first request succeed, which will establish the connection.
+    EXPECT_FALSE(ddns_forwarder.isConnected());
+    createAndSendRequest(RRType::SOA(), Opcode::UPDATE());
+    EXPECT_TRUE(ddns_forwarder.isConnected());
+
+    // make connect attempt fail.  It should result in SERVFAIL.  The
+    // connection should be closed.  Use IPv6 address for varying log output.
+    ddns_forwarder.disablePush();
+    createAndSendRequest(RRType::SOA(), Opcode::UPDATE(), Name("example.com"),
+                         RRClass::IN(), IPPROTO_UDP, "2001:db8::2");
+    EXPECT_TRUE(dnsserv.hasAnswer());
+    headerCheck(*parse_message, default_qid, Rcode::SERVFAIL(),
+                Opcode::UPDATE().getCode(), QR_FLAG, 0, 0, 0, 0);
+    EXPECT_FALSE(ddns_forwarder.isConnected());
+
+    // Allow push again.  Connection will be reopened, and the request will
+    // be forwarded successfully.
+    ddns_forwarder.enablePush();
+    createAndSendRequest(RRType::SOA(), Opcode::UPDATE());
+    EXPECT_FALSE(dnsserv.hasAnswer());
+    EXPECT_TRUE(ddns_forwarder.isConnected());
+}
+
+TEST_F(AuthSrvTest, DDNSForwardClose) {
+    scoped_ptr<AuthSrv> tmp_server(new AuthSrv(true, xfrout, ddns_forwarder));
+    UnitTestUtil::createRequestMessage(request_message, Opcode::UPDATE(),
+                                       default_qid, Name("example.com"),
+                                       RRClass::IN(), RRType::SOA());
+    createRequestPacket(request_message, IPPROTO_UDP);
+    tmp_server->processMessage(*io_message, *parse_message, *response_obuffer,
+                               &dnsserv);
+    EXPECT_FALSE(dnsserv.hasAnswer());
+    EXPECT_TRUE(ddns_forwarder.isConnected());
+
+    // Destroy the server.  The forwarder should close the connection.
+    tmp_server.reset();
+    EXPECT_FALSE(ddns_forwarder.isConnected());
+}
+
 }

+ 77 - 25
src/bin/auth/tests/command_unittest.cc

@@ -23,6 +23,7 @@
 #include <dns/name.h>
 #include <dns/rrclass.h>
 #include <dns/rrtype.h>
+#include <dns/rrttl.h>
 
 #include <cc/data.h>
 
@@ -32,6 +33,7 @@
 
 #include <asiolink/asiolink.h>
 
+#include <util/unittests/mock_socketsession.h>
 #include <testutils/mockups.h>
 
 #include <cassert>
@@ -51,6 +53,7 @@ using namespace isc::dns;
 using namespace isc::data;
 using namespace isc::datasrc;
 using namespace isc::config;
+using namespace isc::util::unittests;
 using namespace isc::testutils;
 using namespace isc::auth::unittest;
 
@@ -59,7 +62,7 @@ namespace {
 class AuthCommandTest : public ::testing::Test {
 protected:
     AuthCommandTest() :
-        server_(false, xfrout_),
+        server_(false, xfrout_, ddns_forwarder_),
         rcode_(-1),
         expect_rcode_(0),
         itimer_(server_.getIOService())
@@ -68,10 +71,11 @@ protected:
     }
     void checkAnswer(const int expected_code) {
         parseAnswer(rcode_, result_);
-        EXPECT_EQ(expected_code, rcode_);
+        EXPECT_EQ(expected_code, rcode_) << result_->str();
     }
     MockSession statistics_session_;
     MockXfroutClient xfrout_;
+    MockSocketSessionForwarder ddns_forwarder_;
     AuthSrv server_;
     ConstElementPtr result_;
     // The shutdown command parameter
@@ -233,7 +237,14 @@ newZoneChecks(AuthSrv& server) {
               find(Name("ns.test2.example"), RRType::AAAA())->code);
 }
 
-TEST_F(AuthCommandTest, loadZone) {
+TEST_F(AuthCommandTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_loadZone
+#else
+       loadZone
+#endif
+    )
+{
     configureZones(server_);
 
     ASSERT_EQ(0, system(INSTALL_PROG " -c " TEST_DATA_DIR
@@ -250,8 +261,6 @@ TEST_F(AuthCommandTest, loadZone) {
     newZoneChecks(server_);
 }
 
-// This test uses dynamic load of a data source module, and won't work when
-// statically linked.
 TEST_F(AuthCommandTest,
 #ifdef USE_STATIC_LINK
        DISABLED_loadZoneSQLite3
@@ -289,36 +298,57 @@ TEST_F(AuthCommandTest,
                               "      }"
                               "    ]"
                               "  }"
-                              "]}"));
+                              "],"
+                              " \"database_file\": \"" + test_db + "\""
+                              "}"));
     module_session.setLocalConfig(map);
     server_.setConfigSession(&module_session);
 
-    // The loadzone command needs the zone to be already loaded, because
-    // it is used for reloading only
-    AuthSrv::InMemoryClientPtr dsrc(new InMemoryClient());
-    dsrc->addZone(ZoneFinderPtr(new InMemoryZoneFinder(RRClass::IN(),
-                                                       Name("example.org"))));
-    server_.setInMemoryClient(RRClass::IN(), dsrc);
+    server_.updateConfig(map);
+
+    // Check that the A record at www.example.org does not exist
+    ASSERT_TRUE(server_.hasInMemoryClient());
+    EXPECT_EQ(ZoneFinder::NXDOMAIN, server_.getInMemoryClient(RRClass::IN())->
+              findZone(Name("example.org")).zone_finder->
+              find(Name("www.example.org"), RRType::A())->code);
+
+    // Add the record to the underlying sqlite database, by loading
+    // it as a separate datasource, and updating it
+    ConstElementPtr sql_cfg = Element::fromJSON("{ \"type\": \"sqlite3\","
+                                                "\"database_file\": \""
+                                                + test_db + "\"}");
+    DataSourceClientContainer sql_ds("sqlite3", sql_cfg);
+    ZoneUpdaterPtr sql_updater =
+        sql_ds.getInstance().getUpdater(Name("example.org"), false);
+    RRsetPtr rrset(new RRset(Name("www.example.org."), RRClass::IN(),
+                             RRType::A(), RRTTL(60)));
+    rrset->addRdata(rdata::createRdata(rrset->getType(),
+                                       rrset->getClass(),
+                                       "192.0.2.1"));
+    sql_updater->addRRset(*rrset);
+    sql_updater->commit();
+
+    // This new record is in the database now, but should not be in the
+    // memory-datasource yet, so check again
+    EXPECT_EQ(ZoneFinder::NXDOMAIN, server_.getInMemoryClient(RRClass::IN())->
+              findZone(Name("example.org")).zone_finder->
+              find(Name("www.example.org"), RRType::A())->code);
 
     // Now send the command to reload it
     result_ = execAuthServerCommand(server_, "loadzone",
-        Element::fromJSON("{\"origin\": \"example.org\"}"));
+                                    Element::fromJSON(
+                                        "{\"origin\": \"example.org\"}"));
     checkAnswer(0);
 
-    // Get the zone and look if there are data in it (the original one was
-    // empty)
-    ASSERT_TRUE(server_.getInMemoryClient(RRClass::IN()));
+    // And now it should be present too.
     EXPECT_EQ(ZoneFinder::SUCCESS, server_.getInMemoryClient(RRClass::IN())->
               findZone(Name("example.org")).zone_finder->
-              find(Name("example.org"), RRType::SOA())->code);
+              find(Name("www.example.org"), RRType::A())->code);
 
-    // Some error cases. First, the zone has no configuration.
-    dsrc->addZone(ZoneFinderPtr(new InMemoryZoneFinder(RRClass::IN(),
-                                                       Name("example.com"))));
+    // Some error cases. First, the zone has no configuration. (note .com here)
     result_ = execAuthServerCommand(server_, "loadzone",
         Element::fromJSON("{\"origin\": \"example.com\"}"));
     checkAnswer(1);
-
     // The previous zone is not hurt in any way
     EXPECT_EQ(ZoneFinder::SUCCESS, server_.getInMemoryClient(RRClass::IN())->
               findZone(Name("example.org")).zone_finder->
@@ -326,7 +356,8 @@ TEST_F(AuthCommandTest,
 
     module_session.setLocalConfig(Element::fromJSON("{\"datasources\": []}"));
     result_ = execAuthServerCommand(server_, "loadzone",
-        Element::fromJSON("{\"origin\": \"example.org\"}"));
+                                    Element::fromJSON(
+                                        "{\"origin\": \"example.org\"}"));
     checkAnswer(1);
 
     // The previous zone is not hurt in any way
@@ -373,7 +404,14 @@ TEST_F(AuthCommandTest,
               find(Name("example.org"), RRType::SOA())->code);
 }
 
-TEST_F(AuthCommandTest, loadBrokenZone) {
+TEST_F(AuthCommandTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_loadBrokenZone
+#else
+       loadBrokenZone
+#endif
+    )
+{
     configureZones(server_);
 
     ASSERT_EQ(0, system(INSTALL_PROG " -c " TEST_DATA_DIR
@@ -386,7 +424,14 @@ TEST_F(AuthCommandTest, loadBrokenZone) {
     zoneChecks(server_);     // zone shouldn't be replaced
 }
 
-TEST_F(AuthCommandTest, loadUnreadableZone) {
+TEST_F(AuthCommandTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_loadUnreadableZone
+#else
+       loadUnreadableZone
+#endif
+    )
+{
     configureZones(server_);
 
     // install the zone file as unreadable
@@ -419,7 +464,14 @@ TEST_F(AuthCommandTest, loadSqlite3DataSrc) {
     checkAnswer(0);
 }
 
-TEST_F(AuthCommandTest, loadZoneInvalidParams) {
+TEST_F(AuthCommandTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_loadZoneInvalidParams
+#else
+       loadZoneInvalidParams
+#endif
+    )
+{
     configureZones(server_);
 
     // null arg

+ 38 - 12
src/bin/auth/tests/common_unittest.cc

@@ -60,37 +60,63 @@ protected:
             EXPECT_EQ(0, setenv(name.c_str(), value.c_str(), 1));
         }
     }
-    // Test getXfroutSocketPath under given environment
-    void testXfrout(const string& fromBuild, const string& localStateDir,
-                    const string& socketFile, const string& expected)
+    // Test getter functions for a socket file path under given environment
+    void testSocketPath(const string& fromBuild, const string& localStateDir,
+                        const string& socketFile, const string& env_name,
+                        const string& expected, string (*actual_fn)())
     {
         setEnv("B10_FROM_BUILD", fromBuild);
         setEnv("B10_FROM_SOURCE_LOCALSTATEDIR", localStateDir);
-        setEnv("BIND10_XFROUT_SOCKET_FILE", socketFile);
-        EXPECT_EQ(expected, getXfroutSocketPath());
+        setEnv(env_name, socketFile);
+        EXPECT_EQ(expected, actual_fn());
     }
 };
 
 // Test that when we have no special environment, we get the default from prefix
 TEST_F(Paths, xfroutNoEnv) {
-    testXfrout("", "", "", UNIX_SOCKET_FILE);
+    testSocketPath("", "", "", "BIND10_XFROUT_SOCKET_FILE",
+                   UNIX_XFROUT_SOCKET_FILE, getXfroutSocketPath);
+}
+
+TEST_F(Paths, ddnsNoEnv) {
+    testSocketPath("", "", "", "BIND10_DDNS_SOCKET_FILE",
+                   UNIX_DDNS_SOCKET_FILE, getDDNSSocketPath);
 }
 
 // Override by B10_FROM_BUILD
 TEST_F(Paths, xfroutFromBuild) {
-    testXfrout("/from/build", "", "/wrong/path",
-               "/from/build/auth_xfrout_conn");
+    testSocketPath("/from/build", "", "/wrong/path",
+                   "BIND10_XFROUT_SOCKET_FILE", "/from/build/auth_xfrout_conn",
+                   getXfroutSocketPath);
+}
+
+TEST_F(Paths, ddnsFromBuild) {
+    testSocketPath("/from/build", "", "/wrong/path", "BIND10_DDNS_SOCKET_FILE",
+                   "/from/build/ddns_socket", getDDNSSocketPath);
 }
 
 // Override by B10_FROM_SOURCE_LOCALSTATEDIR
 TEST_F(Paths, xfroutLocalStatedir) {
-    testXfrout("/wrong/path", "/state/dir", "/wrong/path",
-               "/state/dir/auth_xfrout_conn");
+    testSocketPath("/wrong/path", "/state/dir", "/wrong/path",
+                   "BIND10_XFROUT_SOCKET_FILE", "/state/dir/auth_xfrout_conn",
+                   getXfroutSocketPath);
 }
 
-// Override by BIND10_XFROUT_SOCKET_FILE explicitly
+TEST_F(Paths, ddnsLocalStatedir) {
+    testSocketPath("/wrong/path", "/state/dir", "/wrong/path",
+                   "BIND10_DDNS_SOCKET_FILE", "/state/dir/ddns_socket",
+                   getDDNSSocketPath);
+}
+
+// Override by BIND10_xxx_SOCKET_FILE explicitly
 TEST_F(Paths, xfroutFromEnv) {
-    testXfrout("", "", "/the/path/to/file", "/the/path/to/file");
+    testSocketPath("", "", "/the/path/to/file", "BIND10_XFROUT_SOCKET_FILE",
+                   "/the/path/to/file", getXfroutSocketPath);
+}
+
+TEST_F(Paths, ddnsFromEnv) {
+    testSocketPath("", "", "/the/path/to/file", "BIND10_DDNS_SOCKET_FILE",
+                   "/the/path/to/file", getDDNSSocketPath);
 }
 
 }

+ 99 - 25
src/bin/auth/tests/config_unittest.cc

@@ -32,6 +32,7 @@
 
 #include "datasrc_util.h"
 
+#include <util/unittests/mock_socketsession.h>
 #include <testutils/mockups.h>
 #include <testutils/portconfig.h>
 #include <testutils/socket_request.h>
@@ -44,6 +45,7 @@ using namespace isc::data;
 using namespace isc::datasrc;
 using namespace isc::asiodns;
 using namespace isc::auth::unittest;
+using namespace isc::util::unittests;
 using namespace isc::testutils;
 
 namespace {
@@ -52,7 +54,7 @@ protected:
     AuthConfigTest() :
         dnss_(),
         rrclass(RRClass::IN()),
-        server(true, xfrout),
+        server(true, xfrout, ddns_forwarder),
         // The empty string is expected value of the parameter of
         // requestSocket, not the app_name (there's no fallback, it checks
         // the empty string is passed).
@@ -63,19 +65,27 @@ protected:
     MockDNSService dnss_;
     const RRClass rrclass;
     MockXfroutClient xfrout;
+    MockSocketSessionForwarder ddns_forwarder;
     AuthSrv server;
     isc::server_common::portconfig::AddressList address_store_;
 private:
     isc::testutils::TestSocketRequestor sock_requestor_;
 };
 
-TEST_F(AuthConfigTest, datasourceConfig) {
+TEST_F(AuthConfigTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_datasourceConfig
+#else
+       datasourceConfig
+#endif
+    )
+{
     // By default, we don't have any in-memory data source.
-    EXPECT_EQ(AuthSrv::InMemoryClientPtr(), server.getInMemoryClient(rrclass));
+    EXPECT_FALSE(server.hasInMemoryClient());
     configureAuthServer(server, Element::fromJSON(
                             "{\"datasources\": [{\"type\": \"memory\"}]}"));
     // after successful configuration, we should have one (with empty zoneset).
-    ASSERT_NE(AuthSrv::InMemoryClientPtr(), server.getInMemoryClient(rrclass));
+    EXPECT_TRUE(server.hasInMemoryClient());
     EXPECT_EQ(0, server.getInMemoryClient(rrclass)->getZoneCount());
 }
 
@@ -96,7 +106,7 @@ TEST_F(AuthConfigTest, versionConfig) {
 }
 
 TEST_F(AuthConfigTest, exceptionGuarantee) {
-    EXPECT_EQ(AuthSrv::InMemoryClientPtr(), server.getInMemoryClient(rrclass));
+    EXPECT_FALSE(server.hasInMemoryClient());
     // This configuration contains an invalid item, which will trigger
     // an exception.
     EXPECT_THROW(configureAuthServer(
@@ -106,7 +116,7 @@ TEST_F(AuthConfigTest, exceptionGuarantee) {
                          " \"no_such_config_var\": 1}")),
                  AuthConfigError);
     // The server state shouldn't change
-    EXPECT_EQ(AuthSrv::InMemoryClientPtr(), server.getInMemoryClient(rrclass));
+    EXPECT_FALSE(server.hasInMemoryClient());
 }
 
 TEST_F(AuthConfigTest, exceptionConversion) {
@@ -176,25 +186,46 @@ protected:
 TEST_F(MemoryDatasrcConfigTest, addZeroDataSrc) {
     parser->build(Element::fromJSON("[]"));
     parser->commit();
-    EXPECT_EQ(AuthSrv::InMemoryClientPtr(), server.getInMemoryClient(rrclass));
+    EXPECT_FALSE(server.hasInMemoryClient());
 }
 
-TEST_F(MemoryDatasrcConfigTest, addEmpty) {
+TEST_F(MemoryDatasrcConfigTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_addEmpty
+#else
+       addEmpty
+#endif
+    )
+{
     // By default, we don't have any in-memory data source.
-    EXPECT_EQ(AuthSrv::InMemoryClientPtr(), server.getInMemoryClient(rrclass));
+    EXPECT_FALSE(server.hasInMemoryClient());
     parser->build(Element::fromJSON("[{\"type\": \"memory\"}]"));
     parser->commit();
     EXPECT_EQ(0, server.getInMemoryClient(rrclass)->getZoneCount());
 }
 
-TEST_F(MemoryDatasrcConfigTest, addZeroZone) {
+TEST_F(MemoryDatasrcConfigTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_addZeroZone
+#else
+       addZeroZone
+#endif
+    )
+{
     parser->build(Element::fromJSON("[{\"type\": \"memory\","
                                     "  \"zones\": []}]"));
     parser->commit();
     EXPECT_EQ(0, server.getInMemoryClient(rrclass)->getZoneCount());
 }
 
-TEST_F(MemoryDatasrcConfigTest, addOneZone) {
+TEST_F(MemoryDatasrcConfigTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_addOneZone
+#else
+       addOneZone
+#endif
+    )
+{
     EXPECT_NO_THROW(parser->build(Element::fromJSON(
                       "[{\"type\": \"memory\","
                       "  \"zones\": [{\"origin\": \"example.com\","
@@ -245,7 +276,14 @@ TEST_F(MemoryDatasrcConfigTest,
                  DataSourceError);
 }
 
-TEST_F(MemoryDatasrcConfigTest, addOneWithFiletypeText) {
+TEST_F(MemoryDatasrcConfigTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_addOneWithFiletypeText
+#else
+       addOneWithFiletypeText
+#endif
+    )
+{
     // Explicitly specifying "text" is okay.
     parser->build(Element::fromJSON(
                       "[{\"type\": \"memory\","
@@ -257,7 +295,14 @@ TEST_F(MemoryDatasrcConfigTest, addOneWithFiletypeText) {
     EXPECT_EQ(1, server.getInMemoryClient(rrclass)->getZoneCount());
 }
 
-TEST_F(MemoryDatasrcConfigTest, addMultiZones) {
+TEST_F(MemoryDatasrcConfigTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_addMultiZones
+#else
+       addMultiZones
+#endif
+    )
+{
     EXPECT_NO_THROW(parser->build(Element::fromJSON(
                       "[{\"type\": \"memory\","
                       "  \"zones\": [{\"origin\": \"example.com\","
@@ -273,7 +318,14 @@ TEST_F(MemoryDatasrcConfigTest, addMultiZones) {
     EXPECT_EQ(3, server.getInMemoryClient(rrclass)->getZoneCount());
 }
 
-TEST_F(MemoryDatasrcConfigTest, replace) {
+TEST_F(MemoryDatasrcConfigTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_replace
+#else
+       replace
+#endif
+    )
+{
     EXPECT_NO_THROW(parser->build(Element::fromJSON(
                       "[{\"type\": \"memory\","
                       "  \"zones\": [{\"origin\": \"example.com\","
@@ -304,7 +356,14 @@ TEST_F(MemoryDatasrcConfigTest, replace) {
                   Name("example.com")).code);
 }
 
-TEST_F(MemoryDatasrcConfigTest, exception) {
+TEST_F(MemoryDatasrcConfigTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_exception
+#else
+       exception
+#endif
+    )
+{
     // Load a zone
     EXPECT_NO_THROW(parser->build(Element::fromJSON(
                       "[{\"type\": \"memory\","
@@ -328,7 +387,8 @@ TEST_F(MemoryDatasrcConfigTest, exception) {
                       "/example.org.zone\"},"
                       "              {\"origin\": \"example.net\","
                       "               \"file\": \"" TEST_DATA_DIR
-                      "/nonexistent.zone\"}]}]")), isc::dns::MasterLoadError);
+                      "/nonexistent.zone\"}]}]")),
+                 isc::datasrc::DataSourceError);
     // As that one throwed exception, it is not expected from us to
     // commit it
 
@@ -339,7 +399,14 @@ TEST_F(MemoryDatasrcConfigTest, exception) {
                   Name("example.com")).code);
 }
 
-TEST_F(MemoryDatasrcConfigTest, remove) {
+TEST_F(MemoryDatasrcConfigTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_remove
+#else
+       remove
+#endif
+    )
+{
     EXPECT_NO_THROW(parser->build(Element::fromJSON(
                       "[{\"type\": \"memory\","
                       "  \"zones\": [{\"origin\": \"example.com\","
@@ -352,7 +419,7 @@ TEST_F(MemoryDatasrcConfigTest, remove) {
     parser = createAuthConfigParser(server, "datasources"); 
     EXPECT_NO_THROW(parser->build(Element::fromJSON("[]")));
     EXPECT_NO_THROW(parser->commit());
-    EXPECT_EQ(AuthSrv::InMemoryClientPtr(), server.getInMemoryClient(rrclass));
+    EXPECT_FALSE(server.hasInMemoryClient());
 }
 
 TEST_F(MemoryDatasrcConfigTest, addDuplicateZones) {
@@ -365,7 +432,7 @@ TEST_F(MemoryDatasrcConfigTest, addDuplicateZones) {
                          "              {\"origin\": \"example.com\","
                          "               \"file\": \"" TEST_DATA_DIR
                          "/example.com.zone\"}]}]")),
-                 AuthConfigError);
+                 DataSourceError);
 }
 
 TEST_F(MemoryDatasrcConfigTest, addBadZone) {
@@ -374,35 +441,35 @@ TEST_F(MemoryDatasrcConfigTest, addBadZone) {
                      Element::fromJSON(
                          "[{\"type\": \"memory\","
                          "  \"zones\": [{}]}]")),
-                 AuthConfigError);
+                 DataSourceError);
 
     // origin is missing
     EXPECT_THROW(parser->build(
                      Element::fromJSON(
                          "[{\"type\": \"memory\","
                          "  \"zones\": [{\"file\": \"example.zone\"}]}]")),
-                 AuthConfigError);
+                 DataSourceError);
 
     // file is missing
     EXPECT_THROW(parser->build(
                      Element::fromJSON(
                          "[{\"type\": \"memory\","
                          "  \"zones\": [{\"origin\": \"example.com\"}]}]")),
-                 AuthConfigError);
+                 DataSourceError);
 
     // missing zone file
     EXPECT_THROW(parser->build(
                      Element::fromJSON(
                          "[{\"type\": \"memory\","
                          "  \"zones\": [{\"origin\": \"example.com\"}]}]")),
-                 AuthConfigError);
+                 DataSourceError);
 
     // bogus origin name
     EXPECT_THROW(parser->build(Element::fromJSON(
                       "[{\"type\": \"memory\","
                       "  \"zones\": [{\"origin\": \"example..com\","
                       "               \"file\": \"example.zone\"}]}]")),
-                 AuthConfigError);
+                 DataSourceError);
 
     // bogus RR class name
     EXPECT_THROW(parser->build(
@@ -423,7 +490,14 @@ TEST_F(MemoryDatasrcConfigTest, addBadZone) {
                  isc::InvalidParameter);
 }
 
-TEST_F(MemoryDatasrcConfigTest, badDatasrcType) {
+TEST_F(MemoryDatasrcConfigTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_badDatasrcType
+#else
+       badDatasrcType
+#endif
+    )
+{
     EXPECT_THROW(parser->build(Element::fromJSON("[{\"type\": \"badsrc\"}]")),
                  AuthConfigError);
     EXPECT_THROW(parser->build(Element::fromJSON("[{\"notype\": \"memory\"}]")),

+ 0 - 4
src/bin/auth/tests/query_unittest.cc

@@ -425,10 +425,6 @@ public:
     // answers when DNSSEC is required.
     void setNSEC3Flag(bool on) { use_nsec3_ = on; }
 
-    virtual Name findPreviousName(const Name&) const {
-        isc_throw(isc::NotImplemented, "Mock doesn't support previous name");
-    }
-
     // This method allows tests to insert new record in the middle of the test.
     //
     // \param record_txt textual RR representation of RR (such as soa_txt, etc)

File diff suppressed because it is too large
+ 10 - 63
src/bin/bind10/bind10.8


+ 21 - 1
src/bin/bind10/bind10.xml

@@ -20,7 +20,7 @@
 <refentry>
 
   <refentryinfo>
-    <date>March 1, 2012</date>
+    <date>April 12, 2012</date>
   </refentryinfo>
 
   <refmeta>
@@ -52,6 +52,7 @@
       <arg><option>-u <replaceable>user</replaceable></option></arg>
       <arg><option>-v</option></arg>
       <arg><option>-w <replaceable>wait_time</replaceable></option></arg>
+      <arg><option>--clear-config</option></arg>
       <arg><option>--cmdctl-port</option> <replaceable>port</replaceable></arg>
       <arg><option>--config-file</option> <replaceable>config-filename</replaceable></arg>
       <arg><option>--data-path</option> <replaceable>directory</replaceable></arg>
@@ -106,6 +107,25 @@
 
       <varlistentry>
         <term>
+          <option>--clear-config</option>
+        </term>
+        <listitem>
+	  <para>
+	    This will create a backup of the existing configuration
+	    file, remove it and start
+	    <refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum>
+            with the default configuration.
+	    The name of the backup file can be found in the logs
+	    (<varname>CFGMGR_BACKED_UP_CONFIG_FILE</varname>).
+	    (It will append a number to the backup filename if a
+	    previous backup file exists.)
+
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>
           <option>--cmdctl-port</option> <replaceable>port</replaceable>
         </term>
         <listitem>

+ 9 - 5
src/bin/bind10/bind10_messages.mes

@@ -20,10 +20,6 @@ The boss process is starting up and will now check if the message bus
 daemon is already running. If so, it will not be able to start, as it
 needs a dedicated message bus.
 
-% BIND10_INVALID_STATISTICS_DATA invalid specification of statistics data specified
-An error was encountered when the boss module specified
-statistics data which is invalid for the boss specification file.
-
 % BIND10_COMPONENT_FAILED component %1 (pid %2) failed: %3
 The process terminated, but the bind10 boss didn't expect it to, which means
 it must have failed.
@@ -86,6 +82,10 @@ the boss process will try to force them).
 A debug message. The configurator is about to perform one task of the plan it
 is currently executing on the named component.
 
+% BIND10_INVALID_STATISTICS_DATA invalid specification of statistics data specified
+An error was encountered when the boss module specified
+statistics data which is invalid for the boss specification file.
+
 % BIND10_INVALID_USER invalid user: %1
 The boss process was started with the -u option, to drop root privileges
 and continue running as the specified user, but the user is unknown.
@@ -160,6 +160,11 @@ The boss module is sending a SIGKILL signal to the given process.
 % BIND10_SEND_SIGTERM sending SIGTERM to %1 (PID %2)
 The boss module is sending a SIGTERM signal to the given process.
 
+% BIND10_SETGID setting GID to %1
+The boss switches the process group ID to the given value.  This happens
+when BIND 10 starts with the -u option, and the group ID will be set to
+that of the specified user.
+
 % BIND10_SETUID setting UID to %1
 The boss switches the user it runs as to the given UID.
 
@@ -290,4 +295,3 @@ the configuration manager to start up.  The total length of time Boss
 will wait for the configuration manager before reporting an error is
 set with the command line --wait switch, which has a default value of
 ten seconds.
-

+ 31 - 3
src/bin/bind10/bind10_src.py.in

@@ -64,6 +64,7 @@ import posix
 import copy
 
 from bind10_config import LIBEXECPATH
+import bind10_config
 import isc.cc
 import isc.util.process
 import isc.net.parse
@@ -168,8 +169,8 @@ class BoB:
     
     def __init__(self, msgq_socket_file=None, data_path=None,
                  config_filename=None, clear_config=False, nocache=False,
-                 verbose=False, nokill=False, setuid=None, username=None,
-                 cmdctl_port=None, wait_time=10):
+                 verbose=False, nokill=False, setuid=None, setgid=None,
+                 username=None, cmdctl_port=None, wait_time=10):
         """
             Initialize the Boss of BIND. This is a singleton (only one can run).
         
@@ -207,6 +208,7 @@ class BoB:
         self.components_to_restart = []
         self.runnable = False
         self.uid = setuid
+        self.gid = setgid
         self.username = username
         self.verbose = verbose
         self.nokill = nokill
@@ -1122,6 +1124,28 @@ def unlink_pid_file(pid_file):
         if error.errno is not errno.ENOENT:
             raise
 
+def remove_lock_files():
+    """
+    Remove various lock files which were created by code such as in the
+    logger. This function should be called after BIND 10 shutdown.
+    """
+
+    lockfiles = ["logger_lockfile"]
+
+    lpath = bind10_config.DATA_PATH
+    if "B10_FROM_BUILD" in os.environ:
+        lpath = os.environ["B10_FROM_BUILD"]
+    if "B10_FROM_SOURCE_LOCALSTATEDIR" in os.environ:
+        lpath = os.environ["B10_FROM_SOURCE_LOCALSTATEDIR"]
+    if "B10_LOCKFILE_DIR_FROM_BUILD" in os.environ:
+        lpath = os.environ["B10_LOCKFILE_DIR_FROM_BUILD"]
+
+    for f in lockfiles:
+        fname = lpath + '/' + f
+        if os.path.isfile(fname):
+            os.unlink(fname)
+
+    return
 
 def main():
     global options
@@ -1133,12 +1157,14 @@ def main():
 
     # Check user ID.
     setuid = None
+    setgid = 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
+            setgid = pw_ent.pw_gid
             username = pw_ent.pw_name
         except ValueError:
             pass
@@ -1152,6 +1178,7 @@ def main():
         try:
             pw_ent = pwd.getpwnam(options.user)
             setuid = pw_ent.pw_uid
+            setgid = pw_ent.pw_gid
             username = pw_ent.pw_name
         except KeyError:
             pass
@@ -1182,7 +1209,7 @@ def main():
         boss_of_bind = BoB(options.msgq_socket_file, options.data_path,
                            options.config_file, options.clear_config,
                            options.nocache, options.verbose, options.nokill,
-                           setuid, username, options.cmdctl_port,
+                           setuid, setgid, username, options.cmdctl_port,
                            options.wait_time)
         startup_result = boss_of_bind.startup()
         if startup_result:
@@ -1201,6 +1228,7 @@ def main():
     finally:
         # Clean up the filesystem
         unlink_pid_file(options.pid_file)
+        remove_lock_files()
         if boss_of_bind is not None:
             boss_of_bind.remove_socket_srv()
     sys.exit(boss_of_bind.exitcode)

+ 1 - 0
src/bin/bind10/tests/Makefile.am

@@ -23,6 +23,7 @@ endif
 	chmod +x $(abs_builddir)/$$pytest ; \
 	$(LIBRARY_PATH_PLACEHOLDER) \
 	PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_srcdir)/src/bin:$(abs_top_builddir)/src/bin/bind10:$(abs_top_builddir)/src/lib/util/io/.libs \
+	B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
 	BIND10_MSGQ_SOCKET_FILE=$(abs_top_builddir)/msgq_socket \
 		$(PYCOVERAGE_RUN) $(abs_builddir)/$$pytest || exit ; \
 	done

+ 53 - 6
src/bin/bind10/tests/bind10_test.py.in

@@ -1055,22 +1055,29 @@ class TestPIDFile(unittest.TestCase):
         # dump PID to the file, and confirm the content is correct
         dump_pid(self.pid_file)
         my_pid = os.getpid()
-        self.assertEqual(my_pid, int(open(self.pid_file, "r").read()))
+        with open(self.pid_file, "r") as f:
+            self.assertEqual(my_pid, int(f.read()))
 
     def test_dump_pid(self):
         self.check_pid_file()
 
         # make sure any existing content will be removed
-        open(self.pid_file, "w").write('dummy data\n')
+        with open(self.pid_file, "w") as f:
+            f.write('dummy data\n')
         self.check_pid_file()
 
     def test_unlink_pid_file_notexist(self):
         dummy_data = 'dummy_data\n'
-        open(self.pid_file, "w").write(dummy_data)
+
+        with open(self.pid_file, "w") as f:
+            f.write(dummy_data)
+
         unlink_pid_file("no_such_pid_file")
+
         # the file specified for unlink_pid_file doesn't exist,
         # and the original content of the file should be intact.
-        self.assertEqual(dummy_data, open(self.pid_file, "r").read())
+        with open(self.pid_file, "r") as f:
+            self.assertEqual(dummy_data, f.read())
 
     def test_dump_pid_with_none(self):
         # Check the behavior of dump_pid() and unlink_pid_file() with None.
@@ -1079,9 +1086,14 @@ class TestPIDFile(unittest.TestCase):
         self.assertFalse(os.path.exists(self.pid_file))
 
         dummy_data = 'dummy_data\n'
-        open(self.pid_file, "w").write(dummy_data)
+
+        with open(self.pid_file, "w") as f:
+            f.write(dummy_data)
+
         unlink_pid_file(None)
-        self.assertEqual(dummy_data, open(self.pid_file, "r").read())
+
+        with open(self.pid_file, "r") as f:
+            self.assertEqual(dummy_data, f.read())
 
     def test_dump_pid_failure(self):
         # the attempt to open file will fail, which should result in exception.
@@ -1463,6 +1475,41 @@ class SocketSrvTest(unittest.TestCase):
         self.assertEqual({}, self.__boss._unix_sockets)
         self.assertTrue(sock.closed)
 
+class TestFunctions(unittest.TestCase):
+    def setUp(self):
+        self.lockfile_testpath = \
+            "@abs_top_builddir@/src/bin/bind10/tests/lockfile_test"
+        self.assertFalse(os.path.exists(self.lockfile_testpath))
+        os.mkdir(self.lockfile_testpath)
+        self.assertTrue(os.path.isdir(self.lockfile_testpath))
+
+    def tearDown(self):
+        os.rmdir(self.lockfile_testpath)
+        self.assertFalse(os.path.isdir(self.lockfile_testpath))
+        os.environ["B10_LOCKFILE_DIR_FROM_BUILD"] = "@abs_top_builddir@"
+
+    def test_remove_lock_files(self):
+        os.environ["B10_LOCKFILE_DIR_FROM_BUILD"] = self.lockfile_testpath
+
+        # create lockfiles for the testcase
+        lockfiles = ["logger_lockfile"]
+        for f in lockfiles:
+            fname = os.environ["B10_LOCKFILE_DIR_FROM_BUILD"] + '/' + f
+            self.assertFalse(os.path.exists(fname))
+            open(fname, "w").close()
+            self.assertTrue(os.path.isfile(fname))
+
+        # first call should clear up all the lockfiles
+        bind10_src.remove_lock_files()
+
+        # check if the lockfiles exist
+        for f in lockfiles:
+            fname = os.environ["B10_LOCKFILE_DIR_FROM_BUILD"] + '/' + f
+            self.assertFalse(os.path.isfile(fname))
+
+        # second call should not assert anyway
+        bind10_src.remove_lock_files()
+
 if __name__ == '__main__':
     # store os.environ for test_unchanged_environment
     original_os_environ = copy.deepcopy(os.environ)

+ 7 - 8
src/bin/bindctl/bindctl.1

@@ -2,12 +2,12 @@
 .\"     Title: bindctl
 .\"    Author: [see the "AUTHORS" section]
 .\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
-.\"      Date: December 23, 2010
+.\"      Date: June 20, 2012
 .\"    Manual: BIND10
 .\"    Source: BIND10
 .\"  Language: English
 .\"
-.TH "BINDCTL" "1" "December 23, 2010" "BIND10" "BIND10"
+.TH "BINDCTL" "1" "June 20, 2012" "BIND10" "BIND10"
 .\" -----------------------------------------------------------------
 .\" * set default formatting
 .\" -----------------------------------------------------------------
@@ -35,7 +35,7 @@ via its interactive command interpreter\&.
 communicates over a HTTPS REST\-ful interface provided by
 \fBb10-cmdctl\fR(8)\&. The
 \fBb10-cfgmgr\fR(8)
-daemon stores the configurations and defines the commands\&.
+daemon stores the configurations\&.
 .SH "ARGUMENTS"
 .PP
 The arguments are as follows:
@@ -91,9 +91,9 @@ Display the version number and exit\&.
 .SH "AUTHENTICATION"
 .PP
 The tool will authenticate using a username and password\&. On the first successful login, it will save the details to a comma\-separated\-value (CSV) file which will be used for later uses of
-\fBbindctl\fR\&. The file name is
-default_user\&.csv
-located under the directory specified by the \-\-csv\-file\-dir option\&.
+\fBbindctl\fR\&. The file name is "default_user\&.csv" located under the directory specified by the
+\fB\-\-csv\-file\-dir\fR
+option\&.
 .SH "USAGE"
 .PP
 The
@@ -115,8 +115,7 @@ keyword to receive usage assistance for a module or a module\'s command\&.
 The
 \fBquit\fR
 command is used to exit
-\fBbindctl\fR
-(and doesn\'t stop the BIND 10 services)\&.
+\fBbindctl\fR\&. (It doesn\'t stop the BIND 10 services\&.)
 .PP
 The following module is available by default:
 \fBconfig\fR

+ 10 - 9
src/bin/bindctl/bindctl.xml

@@ -2,7 +2,7 @@
                "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
 	       [<!ENTITY mdash "&#8212;">]>
 <!--
- - Copyright (C) 2010  Internet Systems Consortium, Inc. ("ISC")
+ - Copyright (C) 2010-2012  Internet Systems Consortium, Inc. ("ISC")
  -
  - Permission to use, copy, modify, and/or distribute this software for any
  - purpose with or without fee is hereby granted, provided that the above
@@ -20,7 +20,7 @@
 <refentry>
 
   <refentryinfo>
-    <date>December 23, 2010</date>
+    <date>June 20, 2012</date>
   </refentryinfo>
 
   <refmeta>
@@ -74,7 +74,7 @@
       <citerefentry><refentrytitle>b10-cmdctl</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
       The
 <citerefentry><refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum></citerefentry>
-      daemon stores the configurations and defines the commands.
+      daemon stores the configurations.
     </para>
 
   </refsect1>
@@ -120,8 +120,8 @@
 	    The directory name in which the user/password CSV file
             is stored (see AUTHENTICATION).
 	    By default this option doesn't have any value,
-	    in which case the ".bind10" directory under the user's
-            home directory will be used.
+	    in which case the "<filename>.bind10</filename>" directory
+            under the user's home directory will be used.
           </para>
          </listitem>
       </varlistentry>
@@ -167,8 +167,9 @@
       On the first successful login, it will save the details to
       a comma-separated-value (CSV) file
       which will be used for later uses of <command>bindctl</command>.
-      The file name is <filename>default_user.csv</filename>
-      located under the directory specified by the --csv-file-dir option.
+      The file name is "<filename>default_user.csv</filename>"
+      located under the directory specified by the
+      <option>--csv-file-dir</option> option.
     </para>
 
 <!-- TODO: mention HTTPS? -->
@@ -208,8 +209,8 @@
      <para>
        The <command>quit</command>
        command is used to exit
-       <command>bindctl</command>
-       (and doesn't stop the BIND 10 services).
+       <command>bindctl</command>.
+       (It doesn't stop the BIND 10 services.)
      </para>
 
      <para>

+ 0 - 1
src/bin/bindctl/command_sets.py

@@ -92,4 +92,3 @@ def prepare_execute_commands(tool):
         module.add_command(cmd)
 
     tool.add_module_info(module)
-

+ 17 - 13
src/bin/bindctl/tests/bindctl_test.py

@@ -425,6 +425,12 @@ class FakeBindCmdInterpreter(bindcmd.BindCmdInterpreter):
 
 class TestBindCmdInterpreter(unittest.TestCase):
 
+    def setUp(self):
+        self.old_stdout = sys.stdout
+
+    def tearDown(self):
+        sys.stdout = self.old_stdout
+
     def _create_invalid_csv_file(self, csvfilename):
         import csv
         csvfile = open(csvfilename, 'w')
@@ -447,19 +453,17 @@ class TestBindCmdInterpreter(unittest.TestCase):
         self.assertEqual(new_csv_dir, custom_cmd.csv_file_dir)
 
     def test_get_saved_user_info(self):
-        old_stdout = sys.stdout
-        sys.stdout = open(os.devnull, 'w')
-        cmd = bindcmd.BindCmdInterpreter()
-        users = cmd._get_saved_user_info('/notexist', 'csv_file.csv')
-        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)
-        sys.stdout = old_stdout
-
+        with open(os.devnull, 'w') as f:
+            sys.stdout = f
+            cmd = bindcmd.BindCmdInterpreter()
+            users = cmd._get_saved_user_info('/notexist', 'csv_file.csv')
+            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)
 
 class TestCommandLineOptions(unittest.TestCase):
     def setUp(self):

+ 13 - 12
src/bin/cfgmgr/b10-cfgmgr.8

@@ -2,12 +2,12 @@
 .\"     Title: b10-cfgmgr
 .\"    Author: [FIXME: author] [see http://docbook.sf.net/el/author]
 .\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
-.\"      Date: March 10, 2010
+.\"      Date: June 20, 2012
 .\"    Manual: BIND10
 .\"    Source: BIND10
 .\"  Language: English
 .\"
-.TH "B10\-CFGMGR" "8" "March 10, 2010" "BIND10" "BIND10"
+.TH "B10\-CFGMGR" "8" "June 20, 2012" "BIND10" "BIND10"
 .\" -----------------------------------------------------------------
 .\" * set default formatting
 .\" -----------------------------------------------------------------
@@ -22,7 +22,7 @@
 b10-cfgmgr \- Configuration manager
 .SH "SYNOPSIS"
 .HP \w'\fBb10\-cfgmgr\fR\ 'u
-\fBb10\-cfgmgr\fR [\fB\-c\fR\fB\fIconfig\-filename\fR\fR] [\fB\-p\fR\fB\fIdata_path\fR\fR]
+\fBb10\-cfgmgr\fR [\fB\-c\ \fR\fB\fIconfig\-filename\fR\fR] [\fB\-p\ \fR\fB\fIdata_path\fR\fR] [\fB\-\-clear\-config\fR] [\fB\-\-config\-filename\ \fR\fB\fIconfig\-filename\fR\fR] [\fB\-\-data\-path\ \fR\fB\fIdata_path\fR\fR]
 .SH "DESCRIPTION"
 .PP
 The
@@ -42,22 +42,23 @@ C\-Channel connection\&. If this connection is not established,
 will exit\&.
 .PP
 The daemon may be cleanly stopped by sending the SIGTERM signal to the process\&. This shutdown does not notify the subscribers\&.
-.PP
-When it exits, it saves its current configuration to
-/usr/local/var/bind10\-devel/b10\-config\&.db\&.
-
 .SH "ARGUMENTS"
 .PP
 The arguments are as follows:
 .PP
-\fB\-c\fR\fIconfig\-filename\fR, \fB\-\-config\-filename\fR \fIconfig\-filename\fR
+\fB\-\-clear\-config\fR
+.RS 4
+This will create a backup of the existing configuration file, remove it, and
+b10\-cfgmgr(8)
+will use the default configurations\&. The name of the backup file can be found in the logs (\fICFGMGR_BACKED_UP_CONFIG_FILE\fR)\&. (It will append a number to the backup filename if a previous backup file exists\&.)
+.RE
+.PP
+\fB\-c\fR \fIconfig\-filename\fR, \fB\-\-config\-filename\fR \fIconfig\-filename\fR
 .RS 4
-The configuration database filename to use\&. Can be either absolute or relative to data path\&.
-.sp
-Defaults to b10\-config\&.db
+The configuration database filename to use\&. Can be either absolute or relative to data path\&. It defaults to "b10\-config\&.db"\&.
 .RE
 .PP
-\fB\-p\fR\fIdata\-path\fR, \fB\-\-data\-path\fR \fIdata\-path\fR
+\fB\-p\fR \fIdata\-path\fR, \fB\-\-data\-path\fR \fIdata\-path\fR
 .RS 4
 The path where BIND 10 looks for files\&. The configuration file is looked for here, if it is relative\&. If it is absolute, the path is ignored\&.
 .RE

+ 29 - 4
src/bin/cfgmgr/b10-cfgmgr.py.in

@@ -44,11 +44,11 @@ def parse_options(args=sys.argv[1:], Parser=OptionParser):
     parser = Parser()
     parser.add_option("-p", "--data-path", dest="data_path",
                       help="Directory to search for configuration files " +
-                      "(default=" + DATA_PATH + ")", default=DATA_PATH)
+                      "(default=" + DATA_PATH + ")", default=None)
     parser.add_option("-c", "--config-filename", dest="config_file",
                       help="Configuration database filename " +
                       "(default=" + DEFAULT_CONFIG_FILE + ")",
-                      default=DEFAULT_CONFIG_FILE)
+                      default=None)
     parser.add_option("--clear-config", action="store_true",
                       dest="clear_config", default=False,
                       help="Back up the configuration file and start with " +
@@ -85,12 +85,37 @@ def load_plugins(path, cm):
         # Restore the search path
         sys.path = sys.path[1:]
 
+
+def determine_path_and_file(data_path_option, config_file_option):
+    """Given the data path and config file as specified on the command line
+       (or not specified, as may be the case), determine the full path and
+       file to use when starting the config manager;
+       - if neither are given, use defaults
+       - if both are given, use both
+       - if only data path is given, use default file in that path
+       - if only file is given, use cwd() + file (if file happens to
+         be an absolute file name, path will be ignored)
+       Arguments are either a string, or None.
+       Returns a tuple containing (result_path, result_file).
+    """
+    data_path = data_path_option
+    config_file = config_file_option
+    if config_file is None:
+        config_file = DEFAULT_CONFIG_FILE
+        if data_path is None:
+            data_path = DATA_PATH
+    else:
+        if data_path is None:
+            data_path = os.getcwd()
+    return (data_path, config_file)
+
 def main():
     options = parse_options()
     global cm
     try:
-        cm = ConfigManager(options.data_path, options.config_file,
-                           None, options.clear_config)
+        (data_path, config_file) = determine_path_and_file(options.data_path,
+                                                           options.config_file)
+        cm = ConfigManager(data_path, config_file, None, options.clear_config)
         signal.signal(signal.SIGINT, signal_handler)
         signal.signal(signal.SIGTERM, signal_handler)
         cm.read_config()

+ 28 - 14
src/bin/cfgmgr/b10-cfgmgr.xml

@@ -20,7 +20,7 @@
 <refentry>
 
   <refentryinfo>
-    <date>March 10, 2010</date>
+    <date>June 20, 2012</date>
   </refentryinfo>
 
   <refmeta>
@@ -44,8 +44,11 @@
   <refsynopsisdiv>
     <cmdsynopsis>
       <command>b10-cfgmgr</command>
-      <arg><option>-c<replaceable>config-filename</replaceable></option></arg>
-      <arg><option>-p<replaceable>data_path</replaceable></option></arg>
+      <arg><option>-c <replaceable>config-filename</replaceable></option></arg>
+      <arg><option>-p <replaceable>data_path</replaceable></option></arg>
+      <arg><option>--clear-config</option></arg>
+      <arg><option>--config-filename <replaceable>config-filename</replaceable></option></arg>
+      <arg><option>--data-path <replaceable>data_path</replaceable></option></arg>
     </cmdsynopsis>
   </refsynopsisdiv>
 
@@ -77,13 +80,6 @@
       subscribers.
     </para>
 
-    <para>
-      When it exits, it saves its current configuration to
-      <filename>/usr/local/var/bind10-devel/b10-config.db</filename>.
-<!-- TODO: fix path -->
-<!-- TODO: does it periodically save configuration? -->
-    </para>
-
 <!-- TODO: add a verbose or quiet switch so it is not so noisy -->
   </refsect1>
 
@@ -95,19 +91,37 @@
     <variablelist>
       <varlistentry>
         <term>
-          <option>-c</option><replaceable>config-filename</replaceable>,
+          <option>--clear-config</option>
+        </term>
+        <listitem>
+          <para>
+            This will create a backup of the existing configuration
+            file, remove it, and
+            <refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum>
+            will use the default configurations.
+            The name of the backup file can be found in the logs
+            (<varname>CFGMGR_BACKED_UP_CONFIG_FILE</varname>).
+            (It will append a number to the backup filename if a
+            previous backup file exists.)
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>
+          <option>-c</option> <replaceable>config-filename</replaceable>,
           <option>--config-filename</option> <replaceable>config-filename</replaceable>
         </term>
         <listitem>
           <para>The configuration database filename to use. Can be either
-          absolute or relative to data path.</para>
-          <para>Defaults to b10-config.db</para>
+          absolute or relative to data path.
+          It defaults to "<filename>b10-config.db</filename>".</para>
         </listitem>
       </varlistentry>
 
       <varlistentry>
         <term>
-          <option>-p</option><replaceable>data-path</replaceable>,
+          <option>-p</option> <replaceable>data-path</replaceable>,
           <option>--data-path</option> <replaceable>data-path</replaceable>
         </term>
         <listitem>

+ 17 - 7
src/bin/cfgmgr/tests/b10-cfgmgr_test.py.in

@@ -141,8 +141,8 @@ class TestParseArgs(unittest.TestCase):
         # Pass it empty array, not our arguments
         b = __import__("b10-cfgmgr")
         parsed = b.parse_options([], TestOptParser)
-        self.assertEqual(b.DATA_PATH, parsed.data_path)
-        self.assertEqual(b.DEFAULT_CONFIG_FILE, parsed.config_file)
+        self.assertEqual(None, parsed.data_path)
+        self.assertEqual(None, parsed.config_file)
 
     def test_wrong_args(self):
         """
@@ -168,10 +168,10 @@ class TestParseArgs(unittest.TestCase):
         b = __import__("b10-cfgmgr")
         parsed = b.parse_options(['--data-path=/path'], TestOptParser)
         self.assertEqual('/path', parsed.data_path)
-        self.assertEqual(b.DEFAULT_CONFIG_FILE, parsed.config_file)
+        self.assertEqual(None, parsed.config_file)
         parsed = b.parse_options(['-p', '/path'], TestOptParser)
         self.assertEqual('/path', parsed.data_path)
-        self.assertEqual(b.DEFAULT_CONFIG_FILE, parsed.config_file)
+        self.assertEqual(None, parsed.config_file)
         self.assertRaises(OptsError, b.parse_options, ['-p'], TestOptParser)
         self.assertRaises(OptsError, b.parse_options, ['--data-path'],
                           TestOptParser)
@@ -183,22 +183,32 @@ class TestParseArgs(unittest.TestCase):
         b = __import__("b10-cfgmgr")
         parsed = b.parse_options(['--config-filename=filename'],
                                  TestOptParser)
-        self.assertEqual(b.DATA_PATH, parsed.data_path)
+        self.assertEqual(None, parsed.data_path)
         self.assertEqual("filename", parsed.config_file)
         parsed = b.parse_options(['-c', 'filename'], TestOptParser)
-        self.assertEqual(b.DATA_PATH, parsed.data_path)
+        self.assertEqual(None, parsed.data_path)
         self.assertEqual("filename", parsed.config_file)
         self.assertRaises(OptsError, b.parse_options, ['-c'], TestOptParser)
         self.assertRaises(OptsError, b.parse_options, ['--config-filename'],
                           TestOptParser)
 
+    def test_determine_path_and_file(self):
+        b = __import__("b10-cfgmgr")
+        self.assertEqual((b.DATA_PATH, b.DEFAULT_CONFIG_FILE),
+                         b.determine_path_and_file(None, None))
+        self.assertEqual(("/foo", b.DEFAULT_CONFIG_FILE),
+                         b.determine_path_and_file("/foo", None))
+        self.assertEqual((os.getcwd(), "file.config"),
+                         b.determine_path_and_file(None, "file.config"))
+        self.assertEqual(("/foo", "bar"),
+                         b.determine_path_and_file("/foo", "bar"))
+
     def test_clear_config(self):
         b = __import__("b10-cfgmgr")
         parsed = b.parse_options([], TestOptParser)
         self.assertFalse(parsed.clear_config)
         parsed = b.parse_options(['--clear-config'], TestOptParser)
         self.assertTrue(parsed.clear_config)
-        
 
 if __name__ == '__main__':
     unittest.main()

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

@@ -22,5 +22,6 @@ endif
 	PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/cmdctl \
 	CMDCTL_SPEC_PATH=$(abs_top_builddir)/src/bin/cmdctl \
 	CMDCTL_SRC_PATH=$(abs_top_srcdir)/src/bin/cmdctl \
+	B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
 	$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
 	done

+ 5 - 0
src/bin/cmdctl/tests/cmdctl_test.py

@@ -84,6 +84,7 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
         self.handler.rfile = open("check.tmp", 'w+b')
 
     def tearDown(self):
+        sys.stdout.close()
         sys.stdout = self.old_stdout
         self.handler.rfile.close()
         os.remove('check.tmp')
@@ -306,6 +307,7 @@ class TestCommandControl(unittest.TestCase):
         self.cmdctl = MyCommandControl(None, True)
    
     def tearDown(self):
+        sys.stdout.close()
         sys.stdout = self.old_stdout
 
     def _check_config(self, cmdctl):
@@ -427,6 +429,9 @@ class TestSecureHTTPServer(unittest.TestCase):
                                          MyCommandControl, verbose=True)
 
     def tearDown(self):
+        # both sys.stdout and sys.stderr are the same, so closing one is
+        # sufficient
+        sys.stdout.close()
         sys.stdout = self.old_stdout
         sys.stderr = self.old_stderr
 

File diff suppressed because it is too large
+ 17 - 20
src/bin/dbutil/b10-dbutil.8


+ 25 - 21
src/bin/dbutil/b10-dbutil.xml

@@ -20,7 +20,7 @@
 <refentry>
 
   <refentryinfo>
-    <date>March 20, 2012</date>
+    <date>June 20, 2012</date>
   </refentryinfo>
 
   <refmeta>
@@ -60,14 +60,15 @@
   <refsect1>
     <title>DESCRIPTION</title>
     <para>
-      The <command>b10-dbutil</command> utility is a general administration
-      utility for SQL databases. (Currently only SQLite is supported by
-      BIND 10.)  It can report the current verion of the schema, and upgrade
-      an existing database to the latest version of the schema.
+      The <command>b10-dbutil</command> utility is a general
+      administration utility for SQL databases for BIND 10. (Currently
+      only SQLite is supported by BIND 10.)  It can report the
+      current verion of the schema, and upgrade an existing database
+      to the latest version of the schema.
     </para>
 
     <para>
-      <command>b10-dbutil</command> operates in one of two modes, check mode
+      <command>b10-dbutil</command> operates in one of two modesr: check mode
       or upgrade mode.
     </para>
 
@@ -76,9 +77,10 @@
       utility reads the version of the database schema from the database
       and prints it.  It will tell you whether the schema is at the latest
       version supported by BIND 10. Exit status is 0 if the schema is at
-      the correct version, 1 if the schema is at an older version, 2 if
+      the correct version, 1 if the schema is at an older version, or 2 if
       the schema is at a version not yet supported by this version of
-      b10-dbutil. Any higher value indicates an error during command-line
+      <command>b10-dbutil</command>.
+      Any higher value indicates an error during command-line
       parsing or execution.
     </para>
 
@@ -115,8 +117,8 @@
         </term>
         <listitem>
           <para>Selects the version check function, which reports the
-          current version of the database.  This is incompatible
-          with the --upgrade option.
+          current version of the database.  This is mutually exclusive
+          with the <option>--upgrade</option> option.
           </para>
         </listitem>
       </varlistentry>
@@ -126,11 +128,12 @@
          <option>--noconfirm</option>
         </term>
         <listitem>
-          <para>Only valid with --upgrade, this disables the prompt.
+          <para>Only valid with <option>--upgrade</option>, this disables
+          the prompt.
           Normally the utility will print a warning that an upgrade is
           about to take place and request that you type "Yes" to continue.
           If this switch is given on the command line, no prompt will
-          be issued: the utility will just perform the upgrade.
+          be issued and the utility will just perform the upgrade.
           </para>
         </listitem>
       </varlistentry>
@@ -141,15 +144,16 @@
         </term>
         <listitem>
           <para>Selects the upgrade function, which upgrades the database
-          to the latest version of the schema.  This is incompatible
-          with the --upgrade option.
+          to the latest version of the schema.  This is mutually exclusive
+          with the <option>--check</option> option.
           </para>
           <para>
-          The upgrade function will upgrade a BIND 10 database - no matter how
-          old the schema - preserving all data.  A backup file is created
-          before the upgrade (with the same name as the database, but with
-          ".backup" suffixed to it).  If the upgrade fails, this file can
-          be copied back to restore the original database.
+          The upgrade function will upgrade a BIND 10 database &mdash;
+          no matter how old the schema &mdash; preserving all data.
+	  A backup file is created before the upgrade (with the
+	  same name as the database, but with ".backup" suffixed
+	  to it).  If the upgrade fails, this file can be copied
+	  back to restore the original database.
           </para>
         </listitem>
       </varlistentry>
@@ -160,7 +164,7 @@
         </term>
         <listitem>
           <para>Enable verbose mode.  Each SQL command issued by the
-          utility will be printed to stderr before it is executed.</para>
+          utility will be printed to STDERR before it is executed.</para>
         </listitem>
       </varlistentry>
 
@@ -181,7 +185,7 @@
         </term>
         <listitem>
           <para>
-          Name of the database file to check of upgrade.
+          Name of the database file to check or upgrade.
           </para>
         </listitem>
       </varlistentry>

+ 1 - 1
src/bin/dbutil/dbutil.py.in

@@ -196,7 +196,7 @@ UPGRADES = [
     }
 
 # To extend this, leave the above statements in place and add another
-# dictionary to the list.  The "from" version should be (2, 0), the "to" 
+# dictionary to the list.  The "from" version should be (2, 0), the "to"
 # version whatever the version the update is to, and the SQL statements are
 # the statements required to perform the upgrade.  This way, the upgrade
 # program will be able to upgrade both a V1.0 and a V2.0 database.

+ 1 - 0
src/bin/dbutil/tests/Makefile.am

@@ -3,4 +3,5 @@ SUBDIRS = . testdata
 # Tests of the update script.
 
 check-local:
+	B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
 	$(SHELL) $(abs_builddir)/dbutil_test.sh

+ 36 - 25
src/bin/ddns/b10-ddns.8

@@ -2,12 +2,12 @@
 .\"     Title: b10-ddns
 .\"    Author: [FIXME: author] [see http://docbook.sf.net/el/author]
 .\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
-.\"      Date: February 28, 2012
+.\"      Date: June 18, 2012
 .\"    Manual: BIND10
 .\"    Source: BIND10
 .\"  Language: English
 .\"
-.TH "B10\-DDNS" "8" "February 28, 2012" "BIND10" "BIND10"
+.TH "B10\-DDNS" "8" "June 18, 2012" "BIND10" "BIND10"
 .\" -----------------------------------------------------------------
 .\" * set default formatting
 .\" -----------------------------------------------------------------
@@ -29,33 +29,29 @@ The
 \fBb10\-ddns\fR
 daemon provides the BIND 10 Dynamic Update (DDNS) service, as specified in RFC 2136\&. Normally it is started by the
 \fBbind10\fR(8)
-boss process\&. When the
+boss process\&.
+.PP
+When the
 \fBb10\-auth\fR
-DNS server receives a DDNS update,
+authoritative DNS server receives an UPDATE request, it internally forwards the request to
+\fBb10\-ddns\fR, which handles the rest of the request processing\&. When the processing is completed
 \fBb10\-ddns\fR
-updates the zone in the BIND 10 zone data store\&.
-.if n \{\
-.sp
-.\}
-.RS 4
-.it 1 an-trap
-.nr an-no-space-flag 1
-.nr an-break-flag 1
-.br
-.ps +1
-\fBNote\fR
-.ps -1
-.br
-.PP
-Currently installed is a dummy component\&. It does not provide any functionality\&. It is a skeleton implementation that will be expanded later\&.
-.sp .5v
-.RE
+will send a response to the client with the RCODE set to the value as specified in RFC 2136\&. If the zone has been changed as a result, it will internally notify
+\fBb10\-auth\fR
+and
+\fBb10\-xfrout\fR
+so the new version of the zone will be served, and other secondary servers will be notified via the DNS notify protocol\&.
 .PP
 This daemon communicates with BIND 10 over a
 \fBb10-msgq\fR(8)
 C\-Channel connection\&. If this connection is not established,
 \fBb10\-ddns\fR
-will exit\&.
+will exit\&. The
+\fBb10\-ddns\fR
+daemon also depends on some other BIND 10 components (either directly or indirectly):
+\fBb10-auth\fR(8),
+\fBb10-xfrout\fR(8), and
+\fBb10-zonemgr\fR(8)\&.
 .PP
 
 \fBb10\-ddns\fR
@@ -65,9 +61,16 @@ receives its configurations from
 .PP
 The arguments are as follows:
 .PP
+\fB\-h\fR, \fB\-\-help\fR
+.RS 4
+Print the command line arguments and exit\&.
+.RE
+.PP
 \fB\-v\fR, \fB\-\-verbose\fR
 .RS 4
-This value is ignored at this moment, but is provided for compatibility with the bind10 Boss process
+This value is ignored at this moment, but is provided for compatibility with the
+\fBbind10\fR
+Boss process\&.
 .RE
 .SH "CONFIGURATION AND COMMANDS"
 .PP
@@ -75,7 +78,13 @@ The configurable settings are:
 .PP
 
 \fIzones\fR
-The zones option is a named set of zones that can be updated with DDNS\&. Each entry has one element called update_acl, which is a list of access control rules that define update permissions\&. By default this is empty; DDNS must be explicitely enabled per zone\&.
+The zones option is a list of configuration items for specific zones that can be updated with DDNS\&. Each entry is a map that can contain the following items:
+\fIorigin\fR
+is a textual domain name of the zone;
+\fIclass\fR
+(text) is the RR class of the zone; and
+\fIupdate_acl\fR
+is an ACL that controls permission for updates\&. See the BIND 10 Guide for configuration details\&. Note that not listing a zone in this list does not directly mean update requests for the zone are rejected, but the end result is the same because the default ACL for updates is to deny all requests\&.
 .PP
 The module commands are:
 .PP
@@ -91,13 +100,15 @@ argument to select the process ID to stop\&. (Note that the BIND 10 boss process
 \fBb10-auth\fR(8),
 \fBb10-cfgmgr\fR(8),
 \fBb10-msgq\fR(8),
+\fBb10-xfrout\fR(8),
+\fBb10-zonemgr\fR(8),
 \fBbind10\fR(8),
 BIND 10 Guide\&.
 .SH "HISTORY"
 .PP
 The
 \fBb10\-ddns\fR
-daemon was first implemented in December 2011 for the ISC BIND 10 project\&.
+daemon was first implemented in December 2011 for the ISC BIND 10 project\&. The first functional version was released in June 2012\&.
 .SH "COPYRIGHT"
 .br
 Copyright \(co 2011-2012 Internet Systems Consortium, Inc. ("ISC")

+ 51 - 15
src/bin/ddns/b10-ddns.xml

@@ -20,7 +20,7 @@
 <refentry>
 
   <refentryinfo>
-    <date>February 28, 2012</date>
+    <date>June 18, 2012</date>
   </refentryinfo>
 
   <refmeta>
@@ -58,23 +58,33 @@
       Normally it is started by the
       <citerefentry><refentrytitle>bind10</refentrytitle><manvolnum>8</manvolnum></citerefentry>
       boss process.
-      When the <command>b10-auth</command> DNS server receives
-      a DDNS update, <command>b10-ddns</command> updates the zone
-      in the BIND 10 zone data store.
     </para>
 
-    <note><para>
-      Currently installed is a dummy component. It does not provide
-      any functionality. It is a skeleton implementation that
-      will be expanded later.
-<!-- TODO: #1458 -->
-    </para></note>
+    <para>
+      When the <command>b10-auth</command> authoritative DNS server
+      receives an UPDATE request, it internally forwards the request
+      to <command>b10-ddns</command>, which handles the rest of the
+      request processing.
+      When the processing is completed <command>b10-ddns</command>
+      will send a response to the client with the RCODE set to the
+      value as specified in RFC 2136.
+      If the zone has been changed as a result, it will internally
+      notify <command>b10-auth</command> and
+      <command>b10-xfrout</command> so the new version of the zone will
+      be served, and other secondary servers will be notified via the
+      DNS notify protocol.
+    </para>
 
     <para>
       This daemon communicates with BIND 10 over a
       <citerefentry><refentrytitle>b10-msgq</refentrytitle><manvolnum>8</manvolnum></citerefentry>
       C-Channel connection.  If this connection is not established,
       <command>b10-ddns</command> will exit.
+      The <command>b10-ddns</command> daemon also depends on some other
+      BIND 10 components (either directly or indirectly):
+      <citerefentry><refentrytitle>b10-auth</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+      <citerefentry><refentrytitle>b10-xfrout</refentrytitle><manvolnum>8</manvolnum></citerefentry>, and
+      <citerefentry><refentrytitle>b10-zonemgr</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
     </para>
 
     <para>
@@ -92,13 +102,24 @@
 
       <varlistentry>
         <term>
+          <option>-h</option>,
+          <option>--help</option>
+        </term>
+        <listitem>
+          <para>
+            Print the command line arguments and exit.
+          </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
           <option>-v</option>,
           <option>--verbose</option>
         </term>
         <listitem>
           <para>
             This value is ignored at this moment, but is provided for
-            compatibility with the bind10 Boss process
+            compatibility with the <command>bind10</command> Boss process.
           </para>
         </listitem>
       </varlistentry>
@@ -112,10 +133,18 @@
     </para>
     <para>
       <varname>zones</varname>
-      The zones option is a named set of zones that can be updated with
-      DDNS. Each entry has one element called update_acl, which is
-      a list of access control rules that define update permissions.
-      By default this is empty; DDNS must be explicitely enabled per zone.
+      The zones option is a list of configuration items for specific
+      zones that can be updated with DDNS. Each entry is a map that
+      can contain the following items:
+      <varname>origin</varname> is a textual domain name of the zone;
+      <varname>class</varname> (text) is the RR class of the zone; and
+      <varname>update_acl</varname> is an ACL that controls
+      permission for updates.
+      See the BIND 10 Guide for configuration details.
+      Note that not listing a zone in this list does not directly
+      mean update requests for the zone are rejected, but the end
+      result is the same because the default ACL for updates is to
+      deny all requests.
     </para>
 
     <para>
@@ -145,6 +174,12 @@
         <refentrytitle>b10-msgq</refentrytitle><manvolnum>8</manvolnum>
       </citerefentry>,
       <citerefentry>
+        <refentrytitle>b10-xfrout</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>,
+      <citerefentry>
+        <refentrytitle>b10-zonemgr</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>,
+      <citerefentry>
         <refentrytitle>bind10</refentrytitle><manvolnum>8</manvolnum>
       </citerefentry>,
       <citetitle>BIND 10 Guide</citetitle>.
@@ -156,6 +191,7 @@
     <para>
       The <command>b10-ddns</command> daemon was first implemented
       in December 2011 for the ISC BIND 10 project.
+      The first functional version was released in June 2012.
     </para>
   </refsect1>
 </refentry><!--

+ 444 - 30
src/bin/ddns/ddns.py.in

@@ -18,13 +18,23 @@
 
 import sys; sys.path.append ('@@PYTHONPATH@@')
 import isc
+from isc.acl.dns import REQUEST_LOADER
 import bind10_config
 from isc.dns import *
+import isc.ddns.session
+from isc.ddns.zone_config import ZoneConfig
+from isc.ddns.logger import ClientFormatter, ZoneFormatter
 from isc.config.ccsession import *
-from isc.cc import SessionError, SessionTimeout
+from isc.config.module_spec import ModuleSpecError
+from isc.cc import SessionError, SessionTimeout, ProtocolError
 import isc.util.process
 import isc.util.cio.socketsession
+import isc.server_common.tsig_keyring
+from isc.server_common.dns_tcp import DNSTCPContext
+from isc.datasrc import DataSourceClient
+from isc.server_common.auth_command import auth_loadzone_command
 import select
+import time
 import errno
 
 from isc.log_messages.ddns_messages import *
@@ -39,18 +49,39 @@ isc.log.init("b10-ddns")
 logger = isc.log.Logger("ddns")
 TRACE_BASIC = logger.DBGLVL_TRACE_BASIC
 
-DATA_PATH = bind10_config.DATA_PATH
-SOCKET_FILE = DATA_PATH + '/ddns_socket'
+# Well known path settings.  We need to define
+# SPECFILE_LOCATION: ddns configuration spec file
+# SOCKET_FILE: Unix domain socket file to communicate with b10-auth
+# AUTH_SPECFILE_LOCATION: b10-auth configuration spec file (tentatively
+#  necessarily for sqlite3-only-and-older-datasrc-API stuff).  This should be
+#  gone once we migrate to the new API and start using generalized config.
+#
+# If B10_FROM_SOURCE is set in the environment, we use data files
+# from a directory relative to that, otherwise we use the ones
+# installed on the system
 if "B10_FROM_SOURCE" in os.environ:
-    DATA_PATH = os.environ['B10_FROM_SOURCE'] + "/src/bin/ddns"
+    SPECFILE_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/ddns"
+else:
+    PREFIX = "@prefix@"
+    DATAROOTDIR = "@datarootdir@"
+    SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR)
+    SPECFILE_PATH = SPECFILE_PATH.replace("${prefix}", PREFIX)
+
 if "B10_FROM_BUILD" in os.environ:
     if "B10_FROM_SOURCE_LOCALSTATEDIR" in os.environ:
-        SOCKET_FILE = os.environ["B10_FROM_SOURCE_LOCALSTATEDIR"] + \
-            "/ddns_socket"
+        SOCKET_FILE_PATH = os.environ["B10_FROM_SOURCE_LOCALSTATEDIR"]
     else:
-        SOCKET_FILE = os.environ["B10_FROM_BUILD"] + "/ddns_socket"
-SPECFILE_LOCATION = DATA_PATH + "/ddns.spec"
+        SOCKET_FILE_PATH = os.environ["B10_FROM_BUILD"]
+else:
+    SOCKET_FILE_PATH = bind10_config.DATA_PATH
 
+SPECFILE_LOCATION = SPECFILE_PATH + "/ddns.spec"
+SOCKET_FILE = SOCKET_FILE_PATH + '/ddns_socket'
+
+# Cooperating or dependency modules
+AUTH_MODULE_NAME = 'Auth'
+XFROUT_MODULE_NAME = 'Xfrout'
+ZONEMGR_MODULE_NAME = 'Zonemgr'
 
 isc.util.process.rename()
 
@@ -85,7 +116,55 @@ def clear_socket():
     if os.path.exists(SOCKET_FILE):
         os.remove(SOCKET_FILE)
 
+def get_datasrc_client(cc_session):
+    '''Return data source client for update requests.
+
+    This is supposed to have a very short lifetime and should soon be replaced
+    with generic data source configuration framework.  Based on that
+    observation we simply hardcode everything except the SQLite3 database file,
+    which will be retrieved from the auth server configuration (this behavior
+    will also be deprecated).  When something goes wrong with it this function
+    still returns a dummy client so that the caller doesn't have to bother
+    to handle the error (which would also have to be replaced anyway).
+    The caller will subsequently call its find_zone method via an update
+    session object, which will result in an exception, and then result in
+    a SERVFAIL response.
+
+    Once we are ready for introducing the general framework, the whole
+    function will simply be removed.
+
+    '''
+    HARDCODED_DATASRC_CLASS = RRClass.IN()
+    file, is_default = cc_session.get_remote_config_value("Auth",
+                                                          "database_file")
+    # See xfrout.py:get_db_file() for this trick:
+    if is_default and "B10_FROM_BUILD" in os.environ:
+        file = os.environ["B10_FROM_BUILD"] + "/bind10_zones.sqlite3"
+    datasrc_config = '{ "database_file": "' + file + '"}'
+    try:
+        return (HARDCODED_DATASRC_CLASS,
+                DataSourceClient('sqlite3', datasrc_config), file)
+    except isc.datasrc.Error as ex:
+        class DummyDataSourceClient:
+            def __init__(self, ex):
+                self.__ex = ex
+            def find_zone(self, zone_name):
+                raise isc.datasrc.Error(self.__ex)
+        return (HARDCODED_DATASRC_CLASS, DummyDataSourceClient(ex), file)
+
+def add_pause(sec):
+    '''Pause a specified period for inter module synchronization.
+
+    This is a trivial wrapper of time.sleep, but defined as a separate function
+    so tests can customize it.
+    '''
+    time.sleep(sec)
+
 class DDNSServer:
+    # The number of TCP clients that can be handled by the server at the same
+    # time (this should be configurable parameter).
+    TCP_CLIENTS = 10
+
     def __init__(self, cc_session=None):
         '''
         Initialize the DDNS Server.
@@ -102,8 +181,32 @@ class DDNSServer:
                                                   self.config_handler,
                                                   self.command_handler)
 
+        # Initialize configuration with defaults.  Right now 'zones' is the
+        # only configuration, so we simply directly set it here.
         self._config_data = self._cc.get_full_config()
+        self._zone_config = self.__update_zone_config(
+            self._cc.get_default_value('zones'))
         self._cc.start()
+
+        # Internal attributes derived from other modules.  They will be
+        # initialized via dd_remote_xxx below and will be kept updated
+        # through their callbacks.  They are defined as 'protected' so tests
+        # can examine them; but they are essentially private to the class.
+        #
+        # Datasource client used for handling update requests: when set,
+        # should a tuple of RRClass and DataSourceClient.  Constructed and
+        # maintained based on auth configuration.
+        self._datasrc_info = None
+        # A set of secondary zones, retrieved from zonemgr configuration.
+        self._secondary_zones = None
+
+        # Get necessary configurations from remote modules.
+        for mod in [(AUTH_MODULE_NAME, self.__auth_config_handler),
+                    (ZONEMGR_MODULE_NAME, self.__zonemgr_config_handler)]:
+            self.__add_remote_module(mod[0], mod[1])
+        # This should succeed as long as cfgmgr is up.
+        isc.server_common.tsig_keyring.init_keyring(self._cc)
+
         self._shutdown = False
         # List of the session receivers where we get the requests
         self._socksession_receivers = {}
@@ -112,12 +215,54 @@ class DDNSServer:
         self._listen_socket.bind(SOCKET_FILE)
         self._listen_socket.listen(16)
 
+        # Create reusable resources
+        self.__request_msg = Message(Message.PARSE)
+        self.__response_renderer = MessageRenderer()
+
+        # The following attribute(s) are essentially private, but defined as
+        # "protected" so that test code can customize/inspect them.
+        # They should not be overridden/referenced for any other purposes.
+        #
+        # DDNS Protocol handling class.
+        self._UpdateSessionClass = isc.ddns.session.UpdateSession
+        # Outstanding TCP context: fileno=>(context_obj, dst)
+        self._tcp_ctxs = {}
+
+    class InternalError(Exception):
+        '''Exception for internal errors in an update session.
+
+        This exception is expected to be caught within the server class,
+        only used for controling the code flow.
+
+        '''
+        pass
+
     def config_handler(self, new_config):
         '''Update config data.'''
-        # TODO: Handle exceptions and turn them to an error response
-        # (once we have any configuration)
-        answer = create_answer(0)
-        return answer
+        try:
+            if 'zones' in new_config:
+                self._zone_config = \
+                    self.__update_zone_config(new_config['zones'])
+            return create_answer(0)
+        except Exception as ex:
+            # We catch any exception here.  That includes any syntax error
+            # against the configuration spec.  The config interface is too
+            # complicated and it's not clear how much validation is performed
+            # there, so, while assuming it's unlikely to happen, we act
+            # proactively.
+            logger.error(DDNS_CONFIG_HANDLER_ERROR, ex)
+            return create_answer(1, "Failed to handle new configuration: " +
+                                 str(ex))
+
+    def __update_zone_config(self, new_zones_config):
+        '''Handle zones configuration update.'''
+        new_zones = {}
+        for zone_config in new_zones_config:
+            origin = Name(zone_config['origin'])
+            rrclass = RRClass(zone_config['class'])
+            update_acl = zone_config['update_acl']
+            new_zones[(origin, rrclass)] = REQUEST_LOADER.load(update_acl)
+        return new_zones
 
     def command_handler(self, cmd, args):
         '''
@@ -133,6 +278,88 @@ class DDNSServer:
             answer = create_answer(1, "Unknown command: " + str(cmd))
         return answer
 
+    def __add_remote_module(self, mod_name, callback):
+        '''Register interest in other module's config with a callback.'''
+
+        # Due to startup timing, add_remote_config can fail.  We could make it
+        # more sophisticated, but for now we simply retry a few times, each
+        # separated by a short period (3 times and 1 sec, arbitrary chosen,
+        # and hardcoded for now).  In practice this should be more than
+        # sufficient, but if it turns out to be a bigger problem we can
+        # consider more elegant solutions.
+        for n_try in range(0, 3):
+            try:
+                # by_name() version can fail with ModuleSpecError in getting
+                # the module spec because cfgmgr returns a "successful" answer
+                # with empty data if it cannot find the specified module.
+                # This seems to be a deviant behavior (see Trac #2039), but
+                # we need to deal with it.
+                self._cc.add_remote_config_by_name(mod_name, callback)
+                return
+            except (ModuleSpecError, ModuleCCSessionError) as ex:
+                logger.warn(DDNS_GET_REMOTE_CONFIG_FAIL, mod_name, n_try + 1,
+                            ex)
+                last_ex = ex
+                add_pause(1)
+        raise last_ex
+
+    def __auth_config_handler(self, new_config, module_config):
+        logger.info(DDNS_RECEIVED_AUTH_UPDATE)
+
+        # If we've got the config before and the new config doesn't update
+        # the DB file, there's nothing we should do with it.
+        # Note: there seems to be a bug either in bindctl or cfgmgr, and
+        # new_config can contain 'database_file' even if it's not really
+        # updated.  We still perform the check so we can avoid redundant
+        # resetting when the bug is fixed.  The redundant reset itself is not
+        # good, but such configuration update should not happen so often and
+        # it should be acceptable in practice.
+        if self._datasrc_info is not None and \
+                not 'database_file' in new_config:
+            return
+        rrclass, client, db_file = get_datasrc_client(self._cc)
+        self._datasrc_info = (rrclass, client)
+        logger.info(DDNS_AUTH_DBFILE_UPDATE, db_file)
+
+    def __zonemgr_config_handler(self, new_config, module_config):
+        logger.info(DDNS_RECEIVED_ZONEMGR_UPDATE)
+
+        # If we've got the config before and the new config doesn't update
+        # the secondary zone list, there's nothing we should do with it.
+        # (Same note as that for auth's config applies)
+        if self._secondary_zones is not None and \
+                not 'secondary_zones' in new_config:
+            return
+
+        # Get the latest secondary zones.  Use get_remote_config_value() so
+        # it can work for both the initial default case and updates.
+        sec_zones, _ = self._cc.get_remote_config_value(ZONEMGR_MODULE_NAME,
+                                                        'secondary_zones')
+        new_secondary_zones = set()
+        try:
+            # Parse the new config and build a new list of secondary zones.
+            # Unfortunately, in the current implementation, even an observer
+            # module needs to perform full validation.  This should be changed
+            # so that only post-validation (done by the main module) config is
+            # delivered to observer modules, but until it's supported we need
+            # to protect ourselves.
+            for zone_spec in sec_zones:
+                zname = Name(zone_spec['name'])
+                # class has the default value in case it's unspecified.
+                # ideally this should be merged within the config module, but
+                # the current implementation doesn't esnure that, so we need to
+                # subsitute it ourselves.
+                if 'class' in zone_spec:
+                    zclass = RRClass(zone_spec['class'])
+                else:
+                    zclass = RRClass(module_config.get_default_value(
+                            'secondary_zones/class'))
+                new_secondary_zones.add((zname, zclass))
+            self._secondary_zones = new_secondary_zones
+            logger.info(DDNS_SECONDARY_ZONES_UPDATE, len(self._secondary_zones))
+        except Exception as ex:
+            logger.error(DDNS_SECONDARY_ZONES_UPDATE_FAIL, ex)
+
     def trigger_shutdown(self):
         '''Initiate a shutdown sequence.
 
@@ -150,20 +377,23 @@ class DDNSServer:
         Perform any cleanup that is necessary when shutting down the server.
         Do NOT call this to initialize shutdown, use trigger_shutdown().
 
-        Currently, it only causes the ModuleCCSession to send a message that
-        this module is stopping.
         '''
+        # tell the ModuleCCSession to send a message that this module is
+        # stopping.
         self._cc.send_stopping()
+        # make sure any open socket is explicitly closed, per Python
+        # convention.
+        self._listen_socket.close()
 
     def accept(self):
         """
         Accept another connection and create the session receiver.
         """
         try:
-            sock = self._listen_socket.accept()
+            (sock, remote_addr) = self._listen_socket.accept()
             fileno = sock.fileno()
             logger.debug(TRACE_BASIC, DDNS_NEW_CONN, fileno,
-                         sock.getpeername())
+                         remote_addr if remote_addr else '<anonymous address>')
             receiver = isc.util.cio.socketsession.SocketSessionReceiver(sock)
             self._socksession_receivers[fileno] = (sock, receiver)
         except (socket.error, isc.util.cio.socketsession.SocketSessionError) \
@@ -172,7 +402,30 @@ class DDNSServer:
             # continue with the rest
             logger.error(DDNS_ACCEPT_FAILURE, e)
 
-    def handle_request(self, request):
+    def __check_request_tsig(self, msg, req_data):
+        '''TSIG checker for update requests.
+
+        This is a helper method for handle_request() below.  It examines
+        the given update request message to see if it contains a TSIG RR,
+        and verifies the signature if it does.  It returs the TSIG context
+        used for the verification, or None if the request doesn't contain
+        a TSIG.  If the verification fails it simply raises an exception
+        as handle_request() assumes it should succeed.
+
+        '''
+        tsig_record = msg.get_tsig_record()
+        if tsig_record is None:
+            return None
+        tsig_ctx = TSIGContext(tsig_record.get_name(),
+                               tsig_record.get_rdata().get_algorithm(),
+                               isc.server_common.tsig_keyring.get_keyring())
+        tsig_error = tsig_ctx.verify(tsig_record, req_data)
+        if tsig_error != TSIGError.NOERROR:
+            raise self.InternalError("Failed to verify request's TSIG: " +
+                                     str(tsig_error))
+        return tsig_ctx
+
+    def handle_request(self, req_session):
         """
         This is the place where the actual DDNS processing is done. Other
         methods are either subroutines of this method or methods doing the
@@ -182,34 +435,186 @@ class DDNSServer:
         It is called with the request being session as received from
         SocketSessionReceiver, i.e. tuple
         (socket, local_address, remote_address, data).
+
+        In general, this method doesn't propagate exceptions outside the
+        method.  Most of protocol or system errors will result in an error
+        response to the update client or dropping the update request.
+        The update session class should also ensure this.  Critical exceptions
+        such as memory allocation failure will be propagated, however, and
+        will subsequently terminate the server process.
+
+        Return: True if a response to the request is successfully sent;
+        False otherwise.  The return value wouldn't be useful for the server
+        itself; it's provided mainly for testing purposes.
+
         """
-        # TODO: Implement the magic
+        # give tuple elements intuitive names
+        (sock, local_addr, remote_addr, req_data) = req_session
+
+        # The session sender (b10-auth) should have made sure that this is
+        # a validly formed DNS message of OPCODE being UPDATE, and if it's
+        # TSIG signed, its key is known to the system and the signature is
+        # valid.  Messages that don't meet these should have been resopnded
+        # or dropped by the sender, so if such error is detected we treat it
+        # as an internal error and don't bother to respond.
+        try:
+            self.__request_msg.clear(Message.PARSE)
+            # specify PRESERVE_ORDER as we need to handle each RR separately.
+            self.__request_msg.from_wire(req_data, Message.PRESERVE_ORDER)
+            if self.__request_msg.get_opcode() != Opcode.UPDATE():
+                raise self.InternalError('Update request has unexpected '
+                                         'opcode: ' +
+                                         str(self.__request_msg.get_opcode()))
+            tsig_ctx = self.__check_request_tsig(self.__request_msg, req_data)
+        except Exception as ex:
+            logger.error(DDNS_REQUEST_PARSE_FAIL, ex)
+            return False
+
+        # Let an update session object handle the request.  Note: things around
+        # ZoneConfig will soon be substantially revised.  For now we don't
+        # bother to generalize it.
+        zone_cfg = ZoneConfig(self._secondary_zones, self._datasrc_info[0],
+                              self._datasrc_info[1], self._zone_config)
+        update_session = self._UpdateSessionClass(self.__request_msg,
+                                                  remote_addr, zone_cfg)
+        result, zname, zclass = update_session.handle()
+
+        # If the request should be dropped, we're done; otherwise, send the
+        # response generated by the session object.
+        if result == isc.ddns.session.UPDATE_DROP:
+            return False
+        msg = update_session.get_message()
+        self.__response_renderer.clear()
+        if tsig_ctx is not None:
+            msg.to_wire(self.__response_renderer, tsig_ctx)
+        else:
+            msg.to_wire(self.__response_renderer)
 
-        # TODO: Don't propagate most of the exceptions (like datasrc errors),
-        # just drop the packet.
-        pass
+        ret = self.__send_response(sock, self.__response_renderer.get_data(),
+                                   remote_addr)
+        if result == isc.ddns.session.UPDATE_SUCCESS:
+            self.__notify_auth(zname, zclass)
+            self.__notify_xfrout(zname, zclass)
+        return ret
+
+    def __send_response(self, sock, data, dest):
+        '''Send DDNS response to the client.
+
+        Right now, this is a straightforward subroutine of handle_request(),
+        but is intended to be extended evetually so that it can handle more
+        comlicated operations for TCP (which requires asynchronous write).
+        Further, when we support multiple requests over a single TCP
+        connection, this method may even be shared by multiple methods.
+
+        Parameters:
+        sock: (python socket) the socket to which the response should be sent.
+        data: (binary) the response data
+        dest: (python socket address) the destion address to which the response
+          should be sent.
+
+        Return: True if the send operation succeds; otherwise False.
+
+        '''
+        try:
+            if sock.proto == socket.IPPROTO_UDP:
+                sock.sendto(data, dest)
+            else:
+                tcp_ctx = DNSTCPContext(sock)
+                send_result = tcp_ctx.send(data)
+                if send_result == DNSTCPContext.SENDING:
+                    self._tcp_ctxs[sock.fileno()] = (tcp_ctx, dest)
+                elif send_result == DNSTCPContext.CLOSED:
+                    raise socket.error("socket error in TCP send")
+                else:
+                    tcp_ctx.close()
+        except socket.error as ex:
+            logger.warn(DDNS_RESPONSE_SOCKET_ERROR, ClientFormatter(dest), ex)
+            return False
+
+        return True
+
+    def __notify_auth(self, zname, zclass):
+        '''Notify auth of the update, if necessary.'''
+        msg = auth_loadzone_command(self._cc, zname, zclass)
+        if msg is not None:
+            self.__notify_update(AUTH_MODULE_NAME, msg, zname, zclass)
+
+    def __notify_xfrout(self, zname, zclass):
+        '''Notify xfrout of the update.'''
+        param = {'zone_name': zname.to_text(), 'zone_class': zclass.to_text()}
+        msg = create_command('notify', param)
+        self.__notify_update(XFROUT_MODULE_NAME, msg, zname, zclass)
+
+    def __notify_update(self, modname, msg, zname, zclass):
+        '''Notify other module of the update.
+
+        Note that we use blocking communication here.  While the internal
+        communication bus is generally expected to be pretty responsive and
+        error free, notable delay can still occur, and in worse cases timeouts
+        or connection reset can happen.  In these cases, even if the trouble
+        is temporary, the update service will be suspended for a while.
+        For a longer term we'll need to switch to asynchronous communication,
+        but for now we rely on the blocking operation.
+
+        Note also that we directly refer to the "protected" member of
+        ccsession (_cc._session) rather than creating a separate channel.
+        It's probably not the best practice, but hopefully we can introduce
+        a cleaner way when we support asynchronous communication.
+        At the moment we prefer the brevity with the use of internal channel
+        of the cc session.
+
+        '''
+        try:
+            seq = self._cc._session.group_sendmsg(msg, modname)
+            answer, _ = self._cc._session.group_recvmsg(False, seq)
+            rcode, error_msg = parse_answer(answer)
+        except (SessionTimeout, SessionError, ProtocolError) as ex:
+            rcode = 1
+            error_msg = str(ex)
+        if rcode == 0:
+            logger.debug(TRACE_BASIC, DDNS_UPDATE_NOTIFY, modname,
+                         ZoneFormatter(zname, zclass))
+        else:
+            logger.error(DDNS_UPDATE_NOTIFY_FAIL, modname,
+                         ZoneFormatter(zname, zclass), error_msg)
 
     def handle_session(self, fileno):
-        """
-        Handle incoming session on the socket with given fileno.
+        """Handle incoming session on the socket with given fileno.
+
+        Return True if a response (whether positive or negative) has been
+        sent; otherwise False.  The return value isn't expected to be used
+        for other purposes than testing.
+
         """
         logger.debug(TRACE_BASIC, DDNS_SESSION, fileno)
-        (socket, receiver) = self._socksession_receivers[fileno]
+        (session_socket, receiver) = self._socksession_receivers[fileno]
         try:
-            self.handle_request(receiver.pop())
+            req_session = receiver.pop()
+            (sock, remote_addr) = (req_session[0], req_session[2])
+
+            # If this is a TCP client, check the quota, and immediately reject
+            # it if we cannot accept more.
+            if sock.proto == socket.IPPROTO_TCP and \
+                    len(self._tcp_ctxs) >= self.TCP_CLIENTS:
+                logger.warn(DDNS_REQUEST_TCP_QUOTA,
+                            ClientFormatter(remote_addr), len(self._tcp_ctxs))
+                sock.close()
+                return False
+            return self.handle_request(req_session)
         except isc.util.cio.socketsession.SocketSessionError as se:
             # No matter why this failed, the connection is in unknown, possibly
             # broken state. So, we close the socket and remove the receiver.
             del self._socksession_receivers[fileno]
-            socket.close()
+            session_socket.close()
             logger.warn(DDNS_DROP_CONN, fileno, se)
+            return False
 
     def run(self):
         '''
         Get and process all commands sent from cfgmgr or other modules.
         This loops waiting for events until self.shutdown() has been called.
         '''
-        logger.info(DDNS_RUNNING)
+        logger.info(DDNS_STARTED)
         cc_fileno = self._cc.get_socket().fileno()
         listen_fileno = self._listen_socket.fileno()
         while not self._shutdown:
@@ -223,8 +628,8 @@ class DDNSServer:
             try:
                 (reads, writes, exceptions) = \
                     select.select([cc_fileno, listen_fileno] +
-                                  list(self._socksession_receivers.keys()), [],
-                                  [])
+                                  list(self._socksession_receivers.keys()),
+                                  list(self._tcp_ctxs.keys()), [])
             except select.error as se:
                 # In case it is just interrupted, we continue like nothing
                 # happened
@@ -239,6 +644,15 @@ class DDNSServer:
                     self.accept()
                 else:
                     self.handle_session(fileno)
+            for fileno in writes:
+                ctx = self._tcp_ctxs[fileno]
+                result = ctx[0].send_ready()
+                if result != DNSTCPContext.SENDING:
+                    if result == DNSTCPContext.CLOSED:
+                        logger.warn(DDNS_RESPONSE_TCP_SOCKET_ERROR,
+                                    ClientFormatter(ctx[1]))
+                    ctx[0].close()
+                    del self._tcp_ctxs[fileno]
         self.shutdown_cleanup()
         logger.info(DDNS_STOPPED)
 
@@ -297,7 +711,7 @@ def main(ddns_server=None):
         logger.info(DDNS_STOPPED_BY_KEYBOARD)
     except SessionError as e:
         logger.error(DDNS_CC_SESSION_ERROR, str(e))
-    except ModuleCCSessionError as e:
+    except (ModuleSpecError, ModuleCCSessionError) as e:
         logger.error(DDNS_MODULECC_SESSION_ERROR, str(e))
     except DDNSConfigError as e:
         logger.error(DDNS_CONFIG_ERROR, str(e))

+ 19 - 5
src/bin/ddns/ddns.spec

@@ -4,22 +4,36 @@
     "config_data": [
       {
         "item_name": "zones",
-        "item_type": "named_set",
+        "item_type": "list",
         "item_optional": false,
-        "item_default": {},
-        "named_set_item_spec": {
+        "item_default": [],
+        "list_item_spec": {
           "item_name": "entry",
           "item_type": "map",
           "item_optional": true,
           "item_default": {
-            "update_acl": [{"action": "ACCEPT", "from": "127.0.0.1"},
-                           {"action": "ACCEPT", "from": "::1"}]
+	    "origin": "",
+	    "class": "IN",
+            "update_acl": []
           },
           "map_item_spec": [
             {
+              "item_name": "origin",
+              "item_type": "string",
+              "item_optional": false,
+              "item_default": ""
+            },
+            {
+              "item_name": "class",
+              "item_type": "string",
+              "item_optional": false,
+              "item_default": "IN"
+            },
+            {
               "item_name": "update_acl",
               "item_type": "list",
               "item_optional": false,
+	      "item_default": [],
               "list_item_spec": {
                 "item_name": "acl_element",
                 "item_type": "any",

+ 157 - 3
src/bin/ddns/ddns_messages.mes

@@ -25,6 +25,12 @@ There was a low-level error when we tried to accept an incoming connection
 connections we already have, but this connection is dropped. The reason
 is logged.
 
+% DDNS_AUTH_DBFILE_UPDATE updated auth DB file to %1
+b10-ddns was notified of updates to the SQLite3 DB file that b10-auth
+uses for the underlying data source and on which b10-ddns needs to
+make updates.  b10-ddns then updated its internal setup so further
+updates would be made on the new DB.
+
 % DDNS_CC_SESSION_ERROR error reading from cc channel: %1
 There was a problem reading from the command and control channel. The
 most likely cause is that the msgq process is not running.
@@ -38,6 +44,14 @@ configuration manager b10-cfgmgr is not running.
 The ddns process encountered an error when installing the configuration at
 startup time.  Details of the error are included in the log message.
 
+% DDNS_CONFIG_HANDLER_ERROR failed to update ddns configuration: %1
+An update to b10-ddns configuration was delivered but an error was
+found while applying them.  None of the delivered updates were applied
+to the running b10-ddns system, and the server will keep running with
+the existing configuration.  If this happened in the initial
+configuration setup, the server will be running with the default
+configurations.
+
 % DDNS_DROP_CONN dropping connection on file descriptor %1 because of error %2
 There was an error on a connection with the b10-auth server (or whatever
 connects to the ddns daemon). This might be OK, for example when the
@@ -45,6 +59,29 @@ authoritative server shuts down, the connection would get closed. It also
 can mean the system is busy and can't keep up or that the other side got
 confused and sent bad data.
 
+% DDNS_GET_REMOTE_CONFIG_FAIL failed to get %1 module configuration %2 times: %3
+b10-ddns tried to get configuration of some remote modules for its
+operation, but it failed.  The most likely cause of this is that the
+remote module has not fully started up and b10-ddns couldn't get the
+configuration in a timely fashion.  b10-ddns attempts to retry it a
+few times, imposing a short delay, hoping it eventually succeeds if
+it's just a timing issue.  The number of total failed attempts is also
+logged.  If it reaches an internal threshold b10-ddns considers it a
+fatal error and terminates.  Even in that case, if b10-ddns is
+configured as a "dispensable" component (which is the default), the
+parent bind10 process will restart it, and there will be another
+chance of getting the remote configuration successfully.  These are
+not the optimal behavior, but it's believed to be sufficient in
+practice (there would normally be no failure in the first place).  If
+it really causes an operational trouble other than having a few of
+these log messages, please submit a bug report; there can be several
+ways to make it more sophisticated.  Another, less likely reason for
+having this error is because the remote modules are not actually
+configured to run.  If that's the case fixing the configuration should
+solve the problem - either by making sure the remote module will run
+or by not running b10-ddns (without these remote modules b10-ddns is
+not functional, so there's no point in running it in this case).
+
 % DDNS_MODULECC_SESSION_ERROR error encountered by configuration/command module: %1
 There was a problem in the lower level module handling configuration and
 control commands.  This could happen for various reasons, but the most likely
@@ -58,13 +95,88 @@ requests from it. The file descriptor number and the address where the request
 comes from is logged. The connection is over a unix domain socket and is likely
 coming from a b10-auth process.
 
+% DDNS_RECEIVED_AUTH_UPDATE received configuration updates from auth server
+b10-ddns is notified of updates to b10-auth configuration
+(including a report of the initial configuration) that b10-ddns might
+be interested in.
+
 % DDNS_RECEIVED_SHUTDOWN_COMMAND shutdown command received
 The ddns process received a shutdown command from the command channel
 and will now shut down.
 
-% DDNS_RUNNING ddns server is running and listening for updates
-The ddns process has successfully started and is now ready to receive commands
-and updates.
+% DDNS_RECEIVED_ZONEMGR_UPDATE received configuration updates from zonemgr
+b10-ddns is notified of updates to b10-zonemgr's configuration
+(including a report of the initial configuration).  It may possibly
+contain changes to the secondary zones, in which case b10-ddns will
+update its internal copy of that configuration.
+
+% DDNS_REQUEST_PARSE_FAIL failed to parse update request: %1
+b10-ddns received an update request via b10-auth, but the received
+data failed to pass minimum validation: it was either broken wire
+format data for a valid DNS message (e.g. it's shorter than the
+fixed-length header), or the opcode is not update, or TSIG is included
+in the request but it fails to validate.  Since b10-auth should have
+performed this level of checks, such an error shouldn't be detected at
+this stage and should rather be considered an internal bug.  This
+event is therefore logged at the error level, and the request is
+simply dropped.  Additional information of the error is also logged.
+
+% DDNS_REQUEST_TCP_QUOTA reject TCP update client %1 (%2 running)
+b10-ddns received a new update request from a client over TCP, but
+the number of TCP clients being handled by the server already reached
+the configured quota, so the latest client was rejected by closing
+the connection.  The administrator may want to check the status of
+b10-ddns, and if this happens even if the server is not very busy,
+the quota may have to be increased.  Or, if it's more likely to be
+malicious or simply bogus clients that somehow keep the TCP connection
+open for a long period, maybe they should be rejected with an
+appropriate ACL configuration or some lower layer filtering.  The
+number of existing TCP clients are shown in the log, which should be
+identical to the current quota.
+
+% DDNS_RESPONSE_SOCKET_ERROR failed to send update response to %1: %2
+Network I/O error happens in sending an update response.  The
+client's address that caused the error and error details are also
+logged.
+
+% DDNS_RESPONSE_TCP_SOCKET_ERROR failed to complete sending update response to %1 over TCP
+b10-ddns had tried to send an update response over TCP, and it hadn't
+been completed at that time, and a followup attempt to complete the
+send operation failed due to some network I/O error.  While a network
+error can happen any time, this event is quite unexpected for two
+reasons.  First, since the size of a response to an update request
+should be generally small, it's unlikely that the initial attempt
+didn't fail but wasn't completed.  Second, since the first attempt
+succeeded and the TCP connection had been established in the first
+place, it's more likely for the subsequent attempt to succeed.  In any
+case, there may not be able to do anything to fix it at the server
+side, but the administrator may want to check the general reachability
+with the client address.
+
+% DDNS_SECONDARY_ZONES_UPDATE updated secondary zone list (%1 zones are listed)
+b10-ddns has successfully updated the internal copy of secondary zones
+obtained from b10-zonemgr, based on a latest update to zonemgr's
+configuration.  The number of newly configured (unique) secondary
+zones is logged.
+
+% DDNS_SECONDARY_ZONES_UPDATE_FAIL failed to update secondary zone list: %1
+An error message.  b10-ddns was notified of updates to a list of
+secondary zones from b10-zonemgr and tried to update its own internal
+copy of the list, but it failed.  This can happen if the configuration
+contains an error, and b10-zonemgr should also reject that update.
+Unfortunately, in the current implementation there is no way to ensure
+that both zonemgr and ddns have consistent information when an update
+contains an error; further, as of this writing zonemgr has a bug that
+it could partially update the list of secondary zones if part of the
+list has an error (see Trac ticket #2038).  b10-ddns still keeps
+running with the previous configuration, but it's strongly advisable
+to check log messages from zonemgr, and if it indicates there can be
+inconsistent state, it's better to restart the entire BIND 10 system
+(just restarting b10-ddns wouldn't be enough, because zonemgr can have
+partially updated configuration due to bug #2038).  The log message
+contains an error description, but it's intentionally kept simple as
+it's primarily a matter of zonemgr.  To know the details of the error,
+log messages of zonemgr should be consulted.
 
 % DDNS_SESSION session arrived on file descriptor %1
 A debug message, informing there's some activity on the given file descriptor.
@@ -76,6 +188,10 @@ The ddns process is shutting down. It will no longer listen for new commands
 or updates. Any command or update that is being addressed at this moment will
 be completed, after which the process will exit.
 
+% DDNS_STARTED ddns server is running and listening for updates
+The ddns process has successfully started and is now ready to receive commands
+and updates.
+
 % DDNS_STOPPED ddns server has stopped
 The ddns process has successfully stopped and is no longer listening for or
 handling commands or updates, and will now exit.
@@ -88,3 +204,41 @@ process will now shut down.
 The b10-ddns process encountered an uncaught exception and will now shut
 down. This is indicative of a programming error and should not happen under
 normal circumstances. The exception type and message are printed.
+
+% DDNS_UPDATE_NOTIFY notified %1 of updates to %2
+Debug message.  b10-ddns has made updates to a zone based on an update
+request and has successfully notified an external module of the updates.
+The notified module will use that information for updating its own
+state or any necessary protocol action such as zone reloading or sending
+notify messages to secondary servers.
+
+% DDNS_UPDATE_NOTIFY_FAIL failed to notify %1 of updates to %2: %3
+b10-ddns has made updates to a zone based on an update request and
+tried to notify an external component of the updates, but the
+notification fails.  One possible cause of this is that the external
+component is not really running and it times out in waiting for the
+response, although it will be less likely to happen in practice
+because these components will normally be configured to run when the
+server provides the authoritative DNS service; ddns is rather optional
+among them.  If this happens, however, it will suspend b10-ddns for a
+few seconds during which it cannot handle new requests (some may be
+delayed, some may be dropped, depending on the volume of the incoming
+requests).  This is obviously bad, and if this error happens due to
+this reason, the administrator should make sure the component in
+question should be configured to run.  For a longer term, b10-ddns
+should be more robust about this case such as by making this
+notification asynchronously and/or detecting the existence of the
+external components to avoid hopeless notification in the first place.
+Severity of this error for the receiving components depends on the
+type of the component.  If it's b10-xfrout, this means DNS notify
+messages won't be sent to secondary servers of the zone.  It's
+suboptimal, but not necessarily critical as the secondary servers will
+try to check the zone's status periodically.  If it's b10-auth and the
+notification was needed to have it reload the corresponding zone, it's
+more serious because b10-auth won't be able to serve the new version
+of the zone unless some explicit recovery action is taken.  So the
+administrator needs to examine this message and takes an appropriate
+action.  In either case, this notification is generally expected to
+succeed; so the fact it fails itself means there's something wrong in
+the BIND 10 system, and it would be advisable to check other log
+messages.

+ 1 - 0
src/bin/ddns/tests/Makefile.am

@@ -25,5 +25,6 @@ endif
 	$(LIBRARY_PATH_PLACEHOLDER) \
 	PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/ddns:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/util/io/.libs \
 	TESTDATASRCDIR=$(abs_srcdir)/testdata/ \
+	TESTDATA_PATH=$(abs_top_srcdir)/src/lib/testutils/testdata \
 	$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
 	done

File diff suppressed because it is too large
+ 935 - 12
src/bin/ddns/tests/ddns_test.py


+ 9 - 5
src/bin/dhcp4/Makefile.am

@@ -18,10 +18,10 @@ man_MANS = b10-dhcp4.8
 EXTRA_DIST = $(man_MANS) b10-dhcp4.xml dhcp4.spec
 
 if ENABLE_MAN
-
 b10-dhcp4.8: b10-dhcp4.xml
-	xsltproc --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-dhcp4.xml
-
+	xsltproc --novalid --xinclude --nonet -o $@ \
+        http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl \
+	$(srcdir)/b10-dhcp4.xml
 endif
 
 spec_config.h: spec_config.h.pre
@@ -33,6 +33,12 @@ pkglibexec_PROGRAMS = b10-dhcp4
 b10_dhcp4_SOURCES = main.cc dhcp4_srv.cc dhcp4_srv.h
 b10_dhcp4_SOURCES += ctrl_dhcp4_srv.cc ctrl_dhcp4_srv.h
 
+if USE_CLANGPP
+# Disable unused parameter warning caused by some of the
+# Boost headers when compiling with clang.
+b10_dhcp4_CXXFLAGS = -Wno-unused-parameter
+endif
+
 b10_dhcp4_LDADD = $(top_builddir)/src/lib/dhcp/libdhcp++.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/exceptions/libexceptions.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/asiolink/libasiolink.la
@@ -41,7 +47,5 @@ b10_dhcp4_LDADD += $(top_builddir)/src/lib/config/libcfgclient.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/cc/libcc.la
 
 
-# TODO: config.h.in is wrong because doesn't honor pkgdatadir
-# and can't use @datadir@ because doesn't expand default ${prefix}
 b10_dhcp4dir = $(pkgdatadir)
 b10_dhcp4_DATA = dhcp4.spec

+ 20 - 7
src/bin/dhcp4/main.cc

@@ -18,7 +18,8 @@
 #include <log/dummylog.h>
 #include <log/logger_support.h>
 #include <dhcp4/ctrl_dhcp4_srv.h>
-#include <dhcp/iface_mgr.h>
+#include <dhcp4/dhcp4_srv.h>
+#include <dhcp/dhcp4.h>
 
 using namespace std;
 using namespace isc::dhcp;
@@ -43,7 +44,8 @@ usage() {
     cerr << "Usage:  b10-dhcp4 [-v]"
          << endl;
     cerr << "\t-v: verbose output" << endl;
-    exit(1);
+    cerr << "\t-p number: specify non-standard port number 1-65535 (useful for testing only)" << endl;
+    exit(EXIT_FAILURE);
 }
 } // end of anonymous namespace
 
@@ -51,13 +53,24 @@ int
 main(int argc, char* argv[]) {
     int ch;
     bool verbose_mode = false; // should server be verbose?
+    int port_number = DHCP4_SERVER_PORT; // The default. any other values are
+                                         // useful for testing only.
 
-    while ((ch = getopt(argc, argv, "v")) != -1) {
+    while ((ch = getopt(argc, argv, "vp:")) != -1) {
         switch (ch) {
         case 'v':
             verbose_mode = true;
             isc::log::denabled = true;
             break;
+        case 'p':
+            port_number = strtol(optarg, NULL, 10);
+            if (port_number == 0) {
+                cerr << "Failed to parse port number: [" << optarg
+                     << "], 1-65535 allowed." << endl;
+                usage();
+            }
+            break;
+        case ':':
         default:
             usage();
         }
@@ -68,13 +81,14 @@ main(int argc, char* argv[]) {
                          (verbose_mode ? isc::log::DEBUG : isc::log::INFO),
                          isc::log::MAX_DEBUG_LEVEL, NULL);
 
-    cout << "b10-dhcp4: My pid is " << getpid() << endl;
+    cout << "b10-dhcp4: My pid=" << getpid() << ", binding to port " 
+         << port_number << ", verbose " << (verbose_mode?"yes":"no") << endl;
 
     if (argc - optind > 0) {
         usage();
     }
 
-    int ret = 0;
+    int ret = EXIT_SUCCESS;
 
     try {
 
@@ -83,12 +97,11 @@ main(int argc, char* argv[]) {
         ControlledDhcpv4Srv* server = new ControlledDhcpv4Srv(DHCP4_SERVER_PORT);
         server->run();
         delete server;
-
         server = NULL;
 
     } catch (const std::exception& ex) {
         cerr << "[b10-dhcp4] Server failed: " << ex.what() << endl;
-        ret = 1;
+        ret = EXIT_FAILURE;
     }
 
     return (ret);

+ 24 - 2
src/bin/dhcp4/tests/Makefile.am

@@ -1,12 +1,25 @@
 PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
 
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
+PYTESTS = dhcp4_test.py
+EXTRA_DIST = $(PYTESTS)
+
+# Explicitly specify paths to dynamic libraries required by loadable python
+# modules. That is required on Mac OS systems. Otherwise we will get exception
+# about python not being able to load liblog library.
 LIBRARY_PATH_PLACEHOLDER =
 if SET_ENV_LIBRARY_PATH
 LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
 endif
 
+# test using command-line arguments, so use check-local target instead of TESTS
+check-local:
+	for pytest in $(PYTESTS) ; do \
+	echo Running test: $$pytest ; \
+	PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_srcdir)/src/bin:$(abs_top_builddir)/src/bin/bind10:$(abs_top_builddir)/src/lib/util/io/.libs \
+	$(LIBRARY_PATH_PLACEHOLDER) \
+		$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
+	done
+
 AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
 AM_CPPFLAGS += -I$(top_builddir)/src/bin # for generated spec_config.h header
 AM_CPPFLAGS += -I$(top_srcdir)/src/bin
@@ -25,6 +38,9 @@ if USE_STATIC_LINK
 AM_LDFLAGS = -static
 endif
 
+TESTS_ENVIRONMENT = \
+        libtool --mode=execute $(VALGRIND_COMMAND)
+
 TESTS =
 if HAVE_GTEST
 
@@ -35,6 +51,12 @@ dhcp4_unittests_SOURCES += dhcp4_unittests.cc
 dhcp4_unittests_SOURCES += dhcp4_srv_unittest.cc
 dhcp4_unittests_SOURCES += ctrl_dhcp4_srv_unittest.cc
 
+if USE_CLANGPP
+# Disable unused parameter warning caused by some of the
+# Boost headers when compiling with clang.
+dhcp4_unittests_CXXFLAGS = -Wno-unused-parameter
+endif
+
 dhcp4_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 dhcp4_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
 dhcp4_unittests_LDADD = $(GTEST_LDADD)

+ 170 - 0
src/bin/dhcp4/tests/dhcp4_test.py

@@ -0,0 +1,170 @@
+# Copyright (C) 2012 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.
+
+from bind10_src import ProcessInfo, parse_args, dump_pid, unlink_pid_file, _BASETIME
+
+import unittest
+import sys
+import os
+import signal
+import socket
+from isc.net.addr import IPAddr
+import time
+import isc
+import fcntl
+
+class TestDhcpv4Daemon(unittest.TestCase):
+    def setUp(self):
+        # don't redirect stdout/stderr here as we want to print out things
+        # during the test
+        pass
+
+    def tearDown(self):
+        pass
+
+    def runDhcp4(self, params, wait=1):
+        """
+        This method runs dhcp4 and returns a touple: (returncode, stdout, stderr)
+        """
+        ## @todo: Convert this into generic method and reuse it in dhcp6
+
+        print("Running command: %s" % (" ".join(params)))
+
+        # redirect stdout to a pipe so we can check that our
+        # process spawning is doing the right thing with stdout
+        self.stdout_old = os.dup(sys.stdout.fileno())
+        self.stdout_pipes = os.pipe()
+        os.dup2(self.stdout_pipes[1], sys.stdout.fileno())
+        os.close(self.stdout_pipes[1])
+
+        # do the same trick for stderr:
+        self.stderr_old = os.dup(sys.stderr.fileno())
+        self.stderr_pipes = os.pipe()
+        os.dup2(self.stderr_pipes[1], sys.stderr.fileno())
+        os.close(self.stderr_pipes[1])
+
+        # note that we use dup2() to restore the original stdout
+        # to the main program ASAP in each test... this prevents
+        # hangs reading from the child process (as the pipe is only
+        # open in the child), and also insures nice pretty output
+
+        pi = ProcessInfo('Test Process', params)
+        pi.spawn()
+        time.sleep(wait)
+        os.dup2(self.stdout_old, sys.stdout.fileno())
+        os.dup2(self.stderr_old, sys.stderr.fileno())
+        self.assertNotEqual(pi.process, None)
+        self.assertTrue(type(pi.pid) is int)
+
+        # Set non-blocking read on pipes. Process may not print anything
+        # on specific output and the we would hang without this.
+        fd = self.stdout_pipes[0]
+        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+        fd = self.stderr_pipes[0]
+        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+        # There's potential problem if b10-dhcp4 prints out more
+        # than 4k of text
+        try:
+            output = os.read(self.stdout_pipes[0], 4096)
+        except OSError:
+            print("No data available from stdout")
+            output = ""
+
+        # read can return None. Make sure we have a string
+        if (output is None):
+            output = ""
+
+        try:
+            error = os.read(self.stderr_pipes[0], 4096)
+        except OSError:
+            print("No data available on stderr")
+            error = ""
+
+        # read can return None. Make sure we have a string
+        if (error is None):
+            error = ""
+
+
+        try:
+            if (not pi.process.poll()):
+                # let's be nice at first...
+                pi.process.terminate()
+        except OSError:
+            print("Ignoring failed kill attempt. Process is dead already.")
+
+        # call this to get returncode, process should be dead by now
+        rc = pi.process.wait()
+
+        # Clean up our stdout/stderr munging.
+        os.dup2(self.stdout_old, sys.stdout.fileno())
+        os.close(self.stdout_pipes[0])
+
+        os.dup2(self.stderr_old, sys.stderr.fileno())
+        os.close(self.stderr_pipes[0])
+
+        print ("Process finished, return code=%d, stdout=%d bytes, stderr=%d bytes"
+               % (rc, len(output), len(error)) )
+
+        return (rc, output, error)
+
+    def test_alive(self):
+        print("Note: Purpose of some of the tests is to check if DHCPv4 server can be started,")
+        print("      not that is can bind sockets correctly. Please ignore binding errors.")
+
+        (returncode, output, error) = self.runDhcp4(["../b10-dhcp4", "-v"])
+
+        self.assertEqual( str(output).count("[b10-dhcp4] Initiating DHCPv4 server operation."), 1)
+
+    def test_portnumber_0(self):
+        print("Check that specifying port number 0 is not allowed.")
+
+        (returncode, output, error) = self.runDhcp4(['../b10-dhcp4', '-p', '0'])
+
+        # When invalid port number is specified, return code must not be success
+        self.assertTrue(returncode != 0)
+
+        # Check that there is an error message about invalid port number printed on stderr
+        self.assertEqual( str(error).count("Failed to parse port number"), 1)
+
+    def test_portnumber_missing(self):
+        print("Check that -p option requires a parameter.")
+
+        (returncode, output, error) = self.runDhcp4(['../b10-dhcp4', '-p'])
+
+        # When invalid port number is specified, return code must not be success
+        self.assertTrue(returncode != 0)
+
+        # Check that there is an error message about invalid port number printed on stderr
+        self.assertEqual( str(error).count("option requires an argument"), 1)
+
+    def test_portnumber_nonroot(self):
+        print("Check that specifying unprivileged port number will work.")
+
+        (returncode, output, error) = self.runDhcp4(['../b10-dhcp4', '-p', '10057'])
+
+        # When invalid port number is specified, return code must not be success
+        # TODO: Temporarily commented out as socket binding on systems that do not have
+        #       interface detection implemented currently fails.
+        # self.assertTrue(returncode == 0)
+
+        # Check that there is an error message about invalid port number printed on stderr
+        self.assertEqual( str(output).count("opening sockets on port 10057"), 1)
+
+if __name__ == '__main__':
+    unittest.main()

+ 13 - 9
src/bin/dhcp6/Makefile.am

@@ -2,9 +2,8 @@ SUBDIRS = . tests
 
 AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
 AM_CPPFLAGS += -I$(top_srcdir)/src/bin -I$(top_builddir)/src/bin
-AM_CPPFLAGS += -I$(top_srcdir)/src/lib/dns -I$(top_builddir)/src/lib/dns
 AM_CPPFLAGS += -I$(top_srcdir)/src/lib/cc -I$(top_builddir)/src/lib/cc
- AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CPPFLAGS += $(BOOST_INCLUDES)
 
 AM_CXXFLAGS = $(B10_CXXFLAGS)
 
@@ -14,16 +13,17 @@ endif
 
 pkglibexecdir = $(libexecdir)/@PACKAGE@
 
-CLEANFILES = *.gcno *.gcda spec_config.h
+CLEANFILES = spec_config.h
 
 man_MANS = b10-dhcp6.8
-EXTRA_DIST = $(man_MANS) b10-dhcp6.xml dhcp6.spec interfaces.txt
+EXTRA_DIST = $(man_MANS) b10-dhcp6.xml dhcp6.spec
 
 if ENABLE_MAN
 
 b10-dhcp6.8: b10-dhcp6.xml
-	xsltproc --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-dhcp6.xml
-
+	xsltproc --novalid --xinclude --nonet -o $@ \
+        http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl \
+        $(srcdir)/b10-dhcp6.xml
 endif
 
 spec_config.h: spec_config.h.pre
@@ -34,12 +34,16 @@ pkglibexec_PROGRAMS = b10-dhcp6
 
 b10_dhcp6_SOURCES = main.cc dhcp6_srv.cc dhcp6_srv.h
 
+if USE_CLANGPP
+# Disable unused parameter warning caused by some of the
+# Boost headers when compiling with clang.
+b10_dhcp6_CXXFLAGS = -Wno-unused-parameter
+endif
+
 b10_dhcp6_LDADD = $(top_builddir)/src/lib/exceptions/libexceptions.la
 b10_dhcp6_LDADD += $(top_builddir)/src/lib/asiolink/libasiolink.la
 b10_dhcp6_LDADD += $(top_builddir)/src/lib/log/liblog.la
 b10_dhcp6_LDADD += $(top_builddir)/src/lib/dhcp/libdhcp++.la
 
-# TODO: config.h.in is wrong because doesn't honor pkgdatadir
-# and can't use @datadir@ because doesn't expand default ${prefix}
 b10_dhcp6dir = $(pkgdatadir)
-b10_dhcp6_DATA = dhcp6.spec interfaces.txt
+b10_dhcp6_DATA = dhcp6.spec

+ 1 - 1
src/bin/dhcp6/dhcp6_srv.cc

@@ -40,7 +40,7 @@ const uint32_t HARDCODED_VALID_LIFETIME = 7200; // in seconds
 const std::string HARDCODED_DNS_SERVER = "2001:db8:1::1";
 
 Dhcpv6Srv::Dhcpv6Srv(uint16_t port) {
-    cout << "Initialization" << endl;
+    cout << "Initialization: opening sockets on port " << port << endl;
 
     // first call to instance() will create IfaceMgr (it's a singleton)
     // it may throw something if things go wrong

+ 0 - 10
src/bin/dhcp6/interfaces.txt

@@ -1,10 +0,0 @@
-eth0 fe80::21e:8cff:fe9b:7349
-
-#
-# only first line is read.
-# please use following format:
-# interface-name link-local-ipv6-address
-#
-# This file will become obsolete once proper interface detection 
-# is implemented.
-#

+ 17 - 6
src/bin/dhcp6/main.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2009-2011  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2012  Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -53,20 +53,31 @@ usage() {
     cerr << "Usage:  b10-dhcp6 [-v]"
          << endl;
     cerr << "\t-v: verbose output" << endl;
-    exit(1);
+    cerr << "\t-p number: specify non-standard port number 1-65535 (useful for testing only)" << endl;
+    exit(EXIT_FAILURE);
 }
 } // end of anonymous namespace
 
 int
 main(int argc, char* argv[]) {
     int ch;
+    int port_number = DHCP6_SERVER_PORT; // The default. Any other values are
+                                         // useful for testing only.
 
-    while ((ch = getopt(argc, argv, ":v")) != -1) {
+    while ((ch = getopt(argc, argv, "vp:")) != -1) {
         switch (ch) {
         case 'v':
             verbose_mode = true;
             isc::log::denabled = true;
             break;
+        case 'p':
+            port_number = strtol(optarg, NULL, 10);
+            if (port_number == 0) {
+                cerr << "Failed to parse port number: [" << optarg
+                     << "], 1-65535 allowed." << endl;
+                usage();
+            }
+            break;
         case ':':
         default:
             usage();
@@ -79,7 +90,7 @@ main(int argc, char* argv[]) {
         usage();
     }
 
-    int ret = 0;
+    int ret = EXIT_SUCCESS;
 
     // TODO remainder of auth to dhcp6 code copy. We need to enable this in
     //      dhcp6 eventually
@@ -99,13 +110,13 @@ main(int argc, char* argv[]) {
 
         cout << "[b10-dhcp6] Initiating DHCPv6 operation." << endl;
 
-        Dhcpv6Srv* srv = new Dhcpv6Srv();
+        Dhcpv6Srv* srv = new Dhcpv6Srv(port_number);
 
         srv->run();
 
     } catch (const std::exception& ex) {
         cerr << "[b10-dhcp6] Server failed: " << ex.what() << endl;
-        ret = 1;
+        ret = EXIT_FAILURE;
     }
 
     return (ret);

+ 14 - 9
src/bin/dhcp6/tests/Makefile.am

@@ -1,11 +1,10 @@
 PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-#PYTESTS = args_test.py bind10_test.py
-# NOTE: this has a generated test found in the builddir
 PYTESTS = dhcp6_test.py
 EXTRA_DIST = $(PYTESTS)
 
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
+# Explicitly specify paths to dynamic libraries required by loadable python
+# modules. That is required on Mac OS systems. Otherwise we will get exception
+# about python not being able to load liblog library.
 LIBRARY_PATH_PLACEHOLDER =
 if SET_ENV_LIBRARY_PATH
 LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
@@ -15,9 +14,8 @@ endif
 check-local:
 	for pytest in $(PYTESTS) ; do \
 	echo Running test: $$pytest ; \
-	PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_srcdir)/src/bin:$(abs_top_builddir)/src/bin/bind10:$(abs_top_builddir)/src/lib/util/io/.libs \
 	$(LIBRARY_PATH_PLACEHOLDER) \
-	BIND10_MSGQ_SOCKET_FILE=$(abs_top_builddir)/msgq_socket \
+	PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_srcdir)/src/bin:$(abs_top_builddir)/src/bin/bind10:$(abs_top_builddir)/src/lib/util/io/.libs \
 		$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
 	done
 
@@ -26,8 +24,6 @@ AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
 AM_CPPFLAGS += -I$(top_builddir)/src/bin # for generated spec_config.h header
 AM_CPPFLAGS += -I$(top_srcdir)/src/bin
 AM_CPPFLAGS += $(BOOST_INCLUDES)
-AM_CPPFLAGS += -DTEST_DATA_DIR=\"$(abs_top_srcdir)/src/lib/testutils/testdata\"
-AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/bin/dhcp6/tests\"
 AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
 
 CLEANFILES = $(builddir)/interfaces.txt
@@ -38,6 +34,9 @@ if USE_STATIC_LINK
 AM_LDFLAGS = -static
 endif
 
+TESTS_ENVIRONMENT = \
+        libtool --mode=execute $(VALGRIND_COMMAND)
+
 TESTS =
 if HAVE_GTEST
 
@@ -47,14 +46,20 @@ dhcp6_unittests_SOURCES = ../dhcp6_srv.h ../dhcp6_srv.cc
 dhcp6_unittests_SOURCES += dhcp6_unittests.cc
 dhcp6_unittests_SOURCES += dhcp6_srv_unittest.cc
 
+if USE_CLANGPP
+# Disable unused parameter warning caused by some of the
+# Boost headers when compiling with clang.
+dhcp6_unittests_CXXFLAGS = -Wno-unused-parameter
+endif
+
 dhcp6_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 dhcp6_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
 dhcp6_unittests_LDADD = $(GTEST_LDADD)
-dhcp6_unittests_LDADD += $(SQLITE_LIBS)
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libasiolink.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libdhcp++.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/log/liblog.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libexceptions.la
+
 endif
 
 noinst_PROGRAMS = $(TESTS)

+ 16 - 20
src/bin/dhcp6/tests/dhcp6_srv_unittest.cc

@@ -25,6 +25,7 @@
 #include <dhcp6/dhcp6_srv.h>
 #include <util/buffer.h>
 #include <util/range_utilities.h>
+#include <boost/scoped_ptr.hpp>
 
 using namespace std;
 using namespace isc;
@@ -34,7 +35,6 @@ using namespace isc::util;
 // namespace has to be named, because friends are defined in Dhcpv6Srv class
 // Maybe it should be isc::test?
 namespace {
-const char* const INTERFACE_FILE = "interfaces.txt";
 
 class NakedDhcpv6Srv: public Dhcpv6Srv {
     // "naked" Interface Manager, exposes internal fields
@@ -53,18 +53,10 @@ public:
 
 class Dhcpv6SrvTest : public ::testing::Test {
 public:
+    // these are empty for now, but let's keep them around
     Dhcpv6SrvTest() {
-        unlink(INTERFACE_FILE);
-        fstream fakeifaces(INTERFACE_FILE, ios::out | ios::trunc);
-        if (if_nametoindex("lo") > 0) {
-            fakeifaces << "lo ::1";
-        } else if (if_nametoindex("lo0") > 0) {
-            fakeifaces << "lo0 ::1";
-        }
-        fakeifaces.close();
     }
     ~Dhcpv6SrvTest() {
-        unlink(INTERFACE_FILE);
     };
 };
 
@@ -85,9 +77,9 @@ TEST_F(Dhcpv6SrvTest, basic) {
 TEST_F(Dhcpv6SrvTest, DUID) {
     // tests that DUID is generated properly
 
-    Dhcpv6Srv* srv = NULL;
+    boost::scoped_ptr<Dhcpv6Srv> srv;
     ASSERT_NO_THROW( {
-        srv = new Dhcpv6Srv(DHCP6_SERVER_PORT + 10000);
+        srv.reset(new Dhcpv6Srv(DHCP6_SERVER_PORT + 10000));
     });
 
     OptionPtr srvid = srv->getServerID();
@@ -143,18 +135,24 @@ TEST_F(Dhcpv6SrvTest, DUID) {
     }
     case DUID_LL: {
         // not supported yet
-        cout << "Test not implemented for DUID-LL yet." << endl;
+        cout << "Test not implemented for DUID-LL." << endl;
+
+        // No failure here. There's really no way for test LL DUID. It doesn't
+        // even make sense to check if that Link Layer is actually present on
+        // a physical interface. RFC3315 says a server should write its DUID
+        // and keep it despite hardware changes.
+        break;
     }
     case DUID_UUID: // not supported yet
     default:
-        cout << "Not supported duid type=" << duid_type << endl;
-        FAIL();
+        ADD_FAILURE() << "Not supported duid type=" << duid_type << endl;
+        break;
     }
 }
 
-TEST_F(Dhcpv6SrvTest, DISABLED_Solicit_basic) {
-    NakedDhcpv6Srv* srv = NULL;
-    ASSERT_NO_THROW( srv = new NakedDhcpv6Srv(); );
+TEST_F(Dhcpv6SrvTest, Solicit_basic) {
+    boost::scoped_ptr<NakedDhcpv6Srv> srv;
+    ASSERT_NO_THROW( srv.reset(new NakedDhcpv6Srv()) );
 
     // a dummy content for client-id
     OptionBuffer clntDuid(32);
@@ -223,8 +221,6 @@ TEST_F(Dhcpv6SrvTest, DISABLED_Solicit_basic) {
     EXPECT_TRUE(tmp->getData() == srv->getServerID()->getData());
 
     // more checks to be implemented
-    delete srv;
-
 }
 
 }

+ 126 - 28
src/bin/dhcp6/tests/dhcp6_test.py

@@ -1,4 +1,4 @@
-# Copyright (C) 2011 Internet Systems Consortium.
+# Copyright (C) 2011,2012 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
@@ -23,52 +23,150 @@ import socket
 from isc.net.addr import IPAddr
 import time
 import isc
+import fcntl
 
 class TestDhcpv6Daemon(unittest.TestCase):
     def setUp(self):
+        # don't redirect stdout/stderr here as we want to print out things
+        # during the test
+        pass
+
+    def tearDown(self):
+        pass
+
+    def runCommand(self, params, wait=1):
+        """
+        This method runs a command and returns a touple: (returncode, stdout, stderr)
+        """
+        ## @todo: Convert this into generic method and reuse it in dhcp4 and dhcp6
+
+        print("Running command: %s" % (" ".join(params)))
+
         # redirect stdout to a pipe so we can check that our
         # process spawning is doing the right thing with stdout
-        self.old_stdout = os.dup(sys.stdout.fileno())
-        self.pipes = os.pipe()
-        os.dup2(self.pipes[1], sys.stdout.fileno())
-        os.close(self.pipes[1])
+        self.stdout_old = os.dup(sys.stdout.fileno())
+        self.stdout_pipes = os.pipe()
+        os.dup2(self.stdout_pipes[1], sys.stdout.fileno())
+        os.close(self.stdout_pipes[1])
+
+        # do the same trick for stderr:
+        self.stderr_old = os.dup(sys.stderr.fileno())
+        self.stderr_pipes = os.pipe()
+        os.dup2(self.stderr_pipes[1], sys.stderr.fileno())
+        os.close(self.stderr_pipes[1])
+
         # note that we use dup2() to restore the original stdout
         # to the main program ASAP in each test... this prevents
         # hangs reading from the child process (as the pipe is only
         # open in the child), and also insures nice pretty output
-        print("Please ignore any socket errors. Purpose of this test is to")
-        print("verify that DHCPv6 process could be started, not that socket")
-        print("could be bound. Binding fails when run as non-root user.")
 
-    def tearDown(self):
-        # clean up our stdout munging
-        os.dup2(self.old_stdout, sys.stdout.fileno())
-        os.close(self.pipes[0])
+        pi = ProcessInfo('Test Process', params)
+        pi.spawn()
+        time.sleep(wait)
+        os.dup2(self.stdout_old, sys.stdout.fileno())
+        os.dup2(self.stderr_old, sys.stderr.fileno())
+        self.assertNotEqual(pi.process, None)
+        self.assertTrue(type(pi.pid) is int)
+
+        # Set non-blocking read on pipes. Process may not print anything
+        # on specific output and the we would hang without this.
+        fd = self.stdout_pipes[0]
+        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+        fd = self.stderr_pipes[0]
+        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+        # There's potential problem if b10-dhcp4 prints out more
+        # than 4k of text
+        try:
+            output = os.read(self.stdout_pipes[0], 4096)
+        except OSError:
+            print("No data available from stdout")
+            output = ""
+
+        # read can return None. Make sure we have a string
+        if (output is None):
+            output = ""
+
+        try:
+            error = os.read(self.stderr_pipes[0], 4096)
+        except OSError:
+            print("No data available on stderr")
+            error = ""
+
+        # read can return None. Make sure we have a string
+        if (error is None):
+            error = ""
+
+        try:
+            if (not pi.process.poll()):
+                # let's be nice at first...
+                pi.process.terminate()
+        except OSError:
+            print("Ignoring failed kill attempt. Process is dead already.")
+
+        # call this to get returncode, process should be dead by now
+        rc = pi.process.wait()
+
+        # Clean up our stdout/stderr munging.
+        os.dup2(self.stdout_old, sys.stdout.fileno())
+        os.close(self.stdout_pipes[0])
+
+        os.dup2(self.stderr_old, sys.stderr.fileno())
+        os.close(self.stderr_pipes[0])
+
+        print ("Process finished, return code=%d, stdout=%d bytes, stderr=%d bytes"
+               % (rc, len(output), len(error)) )
+
+        return (rc, output, error)
 
     def test_alive(self):
         """
         Simple test. Checks that b10-dhcp6 can be started and prints out info 
         about starting DHCPv6 operation.
         """
-        pi = ProcessInfo('Test Process', [ '../b10-dhcp6' , '-v' ])
-        pi.spawn()
-        time.sleep(1)
-        os.dup2(self.old_stdout, sys.stdout.fileno())
-        self.assertNotEqual(pi.process, None)
-        self.assertTrue(type(pi.pid) is int)
-        output = os.read(self.pipes[0], 4096)
+        print("Note: Purpose of some of the tests is to check if DHCPv6 server can be started,")
+        print("      not that is can bind sockets correctly. Please ignore binding errors.")
+        (returncode, output, error) = self.runCommand(["../b10-dhcp6", "-v"])
+
         self.assertEqual( str(output).count("[b10-dhcp6] Initiating DHCPv6 operation."), 1)
 
-        # kill this process
-        # XXX: b10-dhcp6 is too dumb to understand 'shutdown' command for now,
-        #      so let's just kill the bastard
+    def test_portnumber_0(self):
+        print("Check that specifying port number 0 is not allowed.")
 
-        # TODO: Ignore errors for now. This test will be more thorough once ticket #1503
-        # (passing port number to b10-dhcp6 daemon) is implemented.
-        try:
-            os.kill(pi.pid, signal.SIGTERM)
-        except OSError:
-            print("Ignoring failed kill attempt. Process is dead already.")
+        (returncode, output, error) = self.runCommand(['../b10-dhcp6', '-p', '0'])
+
+        # When invalid port number is specified, return code must not be success
+        self.assertTrue(returncode != 0)
+
+        # Check that there is an error message about invalid port number printed on stderr
+        self.assertEqual( str(error).count("Failed to parse port number"), 1)
+
+    def test_portnumber_missing(self):
+        print("Check that -p option requires a parameter.")
+
+        (returncode, output, error) = self.runCommand(['../b10-dhcp6', '-p'])
+
+        # When invalid port number is specified, return code must not be success
+        self.assertTrue(returncode != 0)
+
+        # Check that there is an error message about invalid port number printed on stderr
+        self.assertEqual( str(error).count("option requires an argument"), 1)
+
+    def test_portnumber_nonroot(self):
+        print("Check that specifying unprivileged port number will work.")
+
+        (returncode, output, error) = self.runCommand(['../b10-dhcp6', '-p', '10057'])
+
+        # When invalid port number is specified, return code must not be success
+        # TODO: Temporarily commented out as socket binding on systems that do not have
+        #       interface detection implemented currently fails.
+        # self.assertTrue(returncode == 0)
+
+        # Check that there is a message on stdout about opening proper port
+        self.assertEqual( str(output).count("opening sockets on port 10057"), 1)
 
 if __name__ == '__main__':
     unittest.main()

+ 1 - 0
src/bin/msgq/msgq.py.in

@@ -177,6 +177,7 @@ class MsgQ:
             # (note this is a catch-all, but we reraise it)
             if os.path.exists(self.socket_file):
                 os.remove(self.socket_file)
+            self.listen_socket.close()
             raise e
 
         if self.poller:

+ 12 - 0
src/bin/msgq/tests/msgq_test.py

@@ -156,6 +156,12 @@ class SendNonblock(unittest.TestCase):
         except socket.error:
             pass
 
+        # Explicitly close temporary socket pair as the Python
+        # interpreter expects it.  It may not be 100% exception safe,
+        # but since this is only for tests we prefer brevity.
+        read.close()
+        write.close()
+
     def test_infinite_sendmsg(self):
         """
         Tries sending messages (and not reading them) until it either times
@@ -218,6 +224,12 @@ class SendNonblock(unittest.TestCase):
                     os.kill(queue_pid, signal.SIGTERM)
         self.terminate_check(run)
 
+        # Explicitly close temporary socket pair as the Python
+        # interpreter expects it.  It may not be 100% exception safe,
+        # but since this is only for tests we prefer brevity.
+        queue.close()
+        out.close()
+
     def test_small_sends(self):
         """
         Tests sending small data many times.

+ 29 - 29
src/bin/resolver/resolver_messages.mes

@@ -152,6 +152,27 @@ the parsing of the body of the message failed due to some protocol error
 (although the parsing of the header succeeded).  The message parameters
 give a textual description of the problem and the RCODE returned.
 
+% RESOLVER_QUERY_ACCEPTED query accepted: '%1/%2/%3' from %4
+This debug message is produced by the resolver when an incoming query
+is accepted in terms of the query ACL.  The log message shows the query
+in the form of <query name>/<query type>/<query class>, and the client
+that sends the query in the form of <Source IP address>#<source port>.
+
+% RESOLVER_QUERY_DROPPED query dropped: '%1/%2/%3' from %4
+This is an informational message that indicates an incoming query has
+been dropped by the resolver because of the query ACL.  Unlike the
+RESOLVER_QUERY_REJECTED case, the server does not return any response.
+The log message shows the query in the form of <query name>/<query
+type>/<query class>, and the client that sends the query in the form of
+<Source IP address>#<source port>.
+
+% RESOLVER_QUERY_REJECTED query rejected: '%1/%2/%3' from %4
+This is an informational message that indicates an incoming query has
+been rejected by the resolver because of the query ACL.  This results
+in a response with an RCODE of REFUSED. The log message shows the query
+in the form of <query name>/<query type>/<query class>, and the client
+that sends the query in the form of <Source IP address>#<source port>.
+
 % RESOLVER_QUERY_SETUP query setup
 This is a debug message noting that the resolver is creating a
 RecursiveQuery object.
@@ -197,6 +218,10 @@ there comes a time - the lookup timeout - when even the resolver gives up.
 At this point it will wait for pending upstream queries to complete or
 timeout and drop the query.
 
+% RESOLVER_SET_QUERY_ACL query ACL is configured
+This debug message is generated when a new query ACL is configured for
+the resolver.
+
 % RESOLVER_SET_ROOT_ADDRESS setting root address %1(%2)
 This message gives the address of one of the root servers used by the
 resolver.  It is output during startup and may appear multiple times,
@@ -205,6 +230,10 @@ once for each root server address.
 % RESOLVER_SHUTDOWN resolver shutdown complete
 This informational message is output when the resolver has shut down.
 
+% RESOLVER_SHUTDOWN_RECEIVED received command to shut down
+A debug message noting that the server was asked to terminate and is
+complying to the request.
+
 % RESOLVER_STARTED resolver started
 This informational message is output by the resolver when all initialization
 has been completed and it is entering its main loop.
@@ -221,32 +250,3 @@ has been ignored.
 This is debug message output when the resolver received a message with an
 unsupported opcode (it can only process QUERY opcodes).  It will return
 a message to the sender with the RCODE set to NOTIMP.
-
-% RESOLVER_SET_QUERY_ACL query ACL is configured
-This debug message is generated when a new query ACL is configured for
-the resolver.
-
-% RESOLVER_QUERY_ACCEPTED query accepted: '%1/%2/%3' from %4
-This debug message is produced by the resolver when an incoming query
-is accepted in terms of the query ACL.  The log message shows the query
-in the form of <query name>/<query type>/<query class>, and the client
-that sends the query in the form of <Source IP address>#<source port>.
-
-% RESOLVER_QUERY_REJECTED query rejected: '%1/%2/%3' from %4
-This is an informational message that indicates an incoming query has
-been rejected by the resolver because of the query ACL.  This results
-in a response with an RCODE of REFUSED. The log message shows the query
-in the form of <query name>/<query type>/<query class>, and the client
-that sends the query in the form of <Source IP address>#<source port>.
-
-% RESOLVER_QUERY_DROPPED query dropped: '%1/%2/%3' from %4
-This is an informational message that indicates an incoming query has
-been dropped by the resolver because of the query ACL.  Unlike the
-RESOLVER_QUERY_REJECTED case, the server does not return any response.
-The log message shows the query in the form of <query name>/<query
-type>/<query class>, and the client that sends the query in the form of
-<Source IP address>#<source port>.
-
-% RESOLVER_SHUTDOWN_RECEIVED received command to shut down
-A debug message noting that the server was asked to terminate and is
-complying to the request.

+ 3 - 0
src/bin/resolver/tests/Makefile.am

@@ -14,6 +14,9 @@ endif
 
 CLEANFILES = *.gcno *.gcda
 
+TESTS_ENVIRONMENT = \
+        libtool --mode=execute $(VALGRIND_COMMAND)
+
 TESTS =
 if HAVE_GTEST
 TESTS += run_unittests

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

@@ -1,4 +1,4 @@
-SUBDIRS = tests
+SUBDIRS = . tests
 
 AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
 

+ 3 - 0
src/bin/sockcreator/tests/Makefile.am

@@ -8,6 +8,9 @@ if USE_STATIC_LINK
 AM_LDFLAGS = -static
 endif
 
+TESTS_ENVIRONMENT = \
+        libtool --mode=execute $(VALGRIND_COMMAND)
+
 TESTS =
 if HAVE_GTEST
 TESTS += run_unittests

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


+ 8 - 6
src/bin/stats/b10-stats-httpd.xml

@@ -55,16 +55,16 @@
       process runs as a process separated from the process of the BIND 10 Stats
       daemon (<command>b10-stats</command>). The server is initially executed
       by the BIND 10 boss process (<command>bind10</command>) and eventually
-      exited by it.  The server is intended to be server requests by HTTP
+      exited by it.  The server is intended to serve requests by HTTP
       clients like web browsers and third-party modules. When the server is
       asked, it requests BIND 10 statistics data or its schema from
-      <command>b10-stats</command>, and it sends the data back in Python
-      dictionary format and the server converts it into XML format. The server
-      sends it to the HTTP client. The server can send three types of document,
+      <command>b10-stats</command> which sends the data back in Python
+      dictionary format, and the server converts it into XML format. The server
+      sends it to the HTTP client. The server can send three types of documents,
       which are XML (Extensible Markup Language), XSD (XML Schema definition)
       and XSL (Extensible Stylesheet Language). The XML document is the
-      statistics data of BIND 10, The XSD document is the data schema of it,
-      and The XSL document is the style sheet to be showed for the web
+      statistics data of BIND 10, the XSD document is the data schema of it,
+      and the XSL document is the style sheet to be showed for the web
       browsers. There is different URL for each document. But please note that
       you would be redirected to the URL of XML document if you request the URL
       of the root document. For example, you would be redirected to
@@ -82,6 +82,8 @@
     </para>
   </refsect1>
 
+<!-- TODO: this is too verbose; move some of this into the guide instead -->
+
   <refsect1>
     <title>OPTIONS</title>
     <para>The argument is as follow:</para>

+ 11 - 10
src/bin/stats/b10-stats.8

@@ -2,12 +2,12 @@
 .\"     Title: b10-stats
 .\"    Author: [FIXME: author] [see http://docbook.sf.net/el/author]
 .\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
-.\"      Date: March 1, 2012
+.\"      Date: June 20, 2012
 .\"    Manual: BIND10
 .\"    Source: BIND10
 .\"  Language: English
 .\"
-.TH "B10\-STATS" "8" "March 1, 2012" "BIND10" "BIND10"
+.TH "B10\-STATS" "8" "June 20, 2012" "BIND10" "BIND10"
 .\" -----------------------------------------------------------------
 .\" * set default formatting
 .\" -----------------------------------------------------------------
@@ -27,18 +27,21 @@ b10-stats \- BIND 10 statistics module
 .PP
 The
 \fBb10\-stats\fR
-is a daemon forked by
-\fBbind10\fR\&. Stats module collects statistics data from each module and reports statistics information via
-\fBbindctl\fR\&. It communicates by using the Command Channel by
+daemon collects statistics data from each BIND 10 module\&. Its statistics information may be reported via
+\fBbindctl\fR
+or
+\fBb10\-stats\-httpd\fR\&. It is started by
+\fBbind10\fR
+and communicates by using the Command Channel by
 \fBb10\-msgq\fR
 with other modules like
 \fBbind10\fR,
 \fBb10\-auth\fR
-and so on\&. It waits for coming data from other modules, then other modules send data to stats module periodically\&. Other modules send stats data to stats module independently from implementation of stats module, so the frequency of sending data may not be constant\&. Stats module collects data and aggregates it\&.
+and so on\&. It waits for coming data from other modules, then other modules send data to stats module periodically\&. Other modules send stats data to stats module independently from implementation of stats module, so the frequency of sending data may not be constant\&. The stats module collects data and aggregates it\&.
 \fBb10\-stats\fR
 invokes an internal command for
 \fBbind10\fR
-after its initial starting because it\'s sure to collect statistics data from
+after its initial starting to make sure it collects statistics data from
 \fBbind10\fR\&.
 .SH "OPTIONS"
 .PP
@@ -46,9 +49,7 @@ The arguments are as follows:
 .PP
 \fB\-v\fR, \fB\-\-verbose\fR
 .RS 4
-This
-\fBb10\-stats\fR
-switches to verbose mode\&. It sends verbose messages to STDOUT\&.
+This enables maximum debug logging\&.
 .RE
 .SH "CONFIGURATION AND COMMANDS"
 .PP

+ 17 - 18
src/bin/stats/b10-stats.xml

@@ -20,7 +20,7 @@
 <refentry>
 
   <refentryinfo>
-    <date>March 1, 2012</date>
+    <date>June 20, 2012</date>
   </refentryinfo>
 
   <refmeta>
@@ -52,22 +52,22 @@
   <refsect1>
     <title>DESCRIPTION</title>
     <para>
-      The <command>b10-stats</command> is a daemon forked by
-      <command>bind10</command>. Stats module collects statistics data
-      from each module and reports statistics information
-      via <command>bindctl</command>.  It communicates by using the
+      The <command>b10-stats</command> daemon collects statistics data
+      from each BIND 10 module. Its statistics information may be
+      reported via <command>bindctl</command> or
+      <command>b10-stats-httpd</command>.  It is started by
+      <command>bind10</command> and communicates by using the
       Command Channel by <command>b10-msgq</command> with other
-      modules
-      like <command>bind10</command>, <command>b10-auth</command> and
-      so on. It waits for coming data from other modules, then other
-      modules send data to stats module periodically. Other modules
-      send stats data to stats module independently from
-      implementation of stats module, so the frequency of sending data
-      may not be constant. Stats module collects data and aggregates
-      it. <command>b10-stats</command> invokes an internal command
-      for <command>bind10</command> after its initial starting because it's
-      sure to collect statistics data from <command>bind10</command>.
-<!-- TODO: reword that last sentence? -->
+      modules like <command>bind10</command>, <command>b10-auth</command>
+      and so on. It waits for coming data from other modules, then
+      other modules send data to stats module periodically. Other
+      modules send stats data to stats module independently from
+      implementation of stats module, so the frequency of sending
+      data may not be constant. The stats module collects data and
+      aggregates it. <command>b10-stats</command> invokes an internal
+      command for <command>bind10</command> after its initial
+      starting to make sure it collects statistics data from
+      <command>bind10</command>.
     </para>
   </refsect1>
 
@@ -79,8 +79,7 @@
         <term><option>-v</option>, <option>--verbose</option></term>
         <listitem>
 	  <para>
-          This <command>b10-stats</command> switches to verbose
-          mode. It sends verbose messages to STDOUT.
+          This enables maximum debug logging.
 	  </para>
         </listitem>
       </varlistentry>

+ 1 - 1
src/bin/stats/stats.py.in

@@ -495,7 +495,7 @@ if __name__ == "__main__":
         parser = OptionParser()
         parser.add_option(
             "-v", "--verbose", dest="verbose", action="store_true",
-            help="display more about what is going on")
+            help="enable maximum debug logging")
         (options, args) = parser.parse_args()
         if options.verbose:
             isc.log.init("b10-stats", "DEBUG", 99)

+ 1 - 1
src/bin/stats/stats_httpd.py.in

@@ -826,7 +826,7 @@ if __name__ == "__main__":
         parser = OptionParser()
         parser.add_option(
             "-v", "--verbose", dest="verbose", action="store_true",
-            help="display more about what is going on")
+            help="enable maximum debug logging")
         (options, args) = parser.parse_args()
         if options.verbose:
             isc.log.init("b10-stats-httpd", "DEBUG", 99)

+ 16 - 16
src/bin/stats/stats_httpd_messages.mes

@@ -24,14 +24,14 @@ The stats-httpd module was unable to connect to the BIND 10 command
 and control bus. A likely problem is that the message bus daemon
 (b10-msgq) is not running. The stats-httpd module will now shut down.
 
-% STATHTTPD_CLOSING_CC_SESSION stopping cc session
-Debug message indicating that the stats-httpd module is disconnecting
-from the command and control bus.
-
 % STATHTTPD_CLOSING closing %1#%2
 The stats-httpd daemon will stop listening for requests on the given
 address and port number.
 
+% STATHTTPD_CLOSING_CC_SESSION stopping cc session
+Debug message indicating that the stats-httpd module is disconnecting
+from the command and control bus.
+
 % STATHTTPD_HANDLE_CONFIG reading configuration: %1
 The stats-httpd daemon has received new configuration data and will now
 process it. The (changed) data is printed.
@@ -49,18 +49,18 @@ An unknown command has been sent to the stats-httpd module. The
 stats-httpd module will respond with an error, and the command will
 be ignored.
 
-% STATHTTPD_SERVER_ERROR HTTP server error: %1
-An internal error occurred while handling an HTTP request. An HTTP 500
-response will be sent back, and the specific error is printed. This
-is an error condition that likely points to a module that is not
-responding correctly to statistic requests.
-
 % STATHTTPD_SERVER_DATAERROR HTTP server data error: %1
 An internal error occurred while handling an HTTP request. An HTTP 404
 response will be sent back, and the specific error is printed. This
 is an error condition that likely points the specified data
 corresponding to the requested URI is incorrect.
 
+% STATHTTPD_SERVER_ERROR HTTP server error: %1
+An internal error occurred while handling an HTTP request. An HTTP 500
+response will be sent back, and the specific error is printed. This
+is an error condition that likely points to a module that is not
+responding correctly to statistic requests.
+
 % STATHTTPD_SERVER_INIT_ERROR HTTP server initialization error: %1
 There was a problem initializing the HTTP server in the stats-httpd
 module upon receiving its configuration data. The most likely cause
@@ -71,12 +71,6 @@ and an error is sent back.
 % STATHTTPD_SHUTDOWN shutting down
 The stats-httpd daemon is shutting down.
 
-% STATHTTPD_START_SERVER_INIT_ERROR HTTP server initialization error: %1
-There was a problem initializing the HTTP server in the stats-httpd
-module upon startup. The most likely cause is that it was not able
-to bind to the listening port. The specific error is printed, and the
-module will shut down.
-
 % STATHTTPD_STARTED listening on %1#%2
 The stats-httpd daemon will now start listening for requests on the
 given address and port number.
@@ -85,6 +79,12 @@ given address and port number.
 Debug message indicating that the stats-httpd module is connecting to
 the command and control bus.
 
+% STATHTTPD_START_SERVER_INIT_ERROR HTTP server initialization error: %1
+There was a problem initializing the HTTP server in the stats-httpd
+module upon startup. The most likely cause is that it was not able
+to bind to the listening port. The specific error is printed, and the
+module will shut down.
+
 % STATHTTPD_STOPPED_BY_KEYBOARD keyboard interrupt, shutting down
 There was a keyboard interrupt signal to stop the stats-httpd
 daemon. The daemon will now shut down.

+ 13 - 13
src/bin/stats/stats_messages.mes

@@ -28,6 +28,12 @@ control bus. A likely problem is that the message bus daemon
 This debug message is printed when the stats module has received a
 configuration update from the configuration manager.
 
+% STATS_RECEIVED_SHOWSCHEMA_ALL_COMMAND received command to show all statistics schema
+The stats module received a command to show all statistics schemas of all modules.
+
+% STATS_RECEIVED_SHOWSCHEMA_NAME_COMMAND received command to show statistics schema for %1
+The stats module received a command to show the specified statistics schema of the specified module.
+
 % STATS_RECEIVED_SHOW_ALL_COMMAND received command to show all statistics
 The stats module received a command to show all statistics that it has
 collected.
@@ -51,6 +57,13 @@ will respond with an error and the command will be ignored.
 This debug message is printed when a request is sent to the boss module
 to send its data to the stats module.
 
+% STATS_STARTING starting
+The stats module will be now starting.
+
+% STATS_START_ERROR stats module error: %1
+An internal error occurred while starting the stats module. The stats
+module will be now shutting down.
+
 % STATS_STOPPED_BY_KEYBOARD keyboard interrupt, shutting down
 There was a keyboard interrupt signal to stop the stats module. The
 daemon will now shut down.
@@ -61,16 +74,3 @@ is unknown in the implementation. The most likely cause is an
 installation problem, where the specification file stats.spec is
 from a different version of BIND 10 than the stats module itself.
 Please check your installation.
-
-% STATS_STARTING starting
-The stats module will be now starting.
-
-% STATS_RECEIVED_SHOWSCHEMA_ALL_COMMAND received command to show all statistics schema
-The stats module received a command to show all statistics schemas of all modules.
-
-% STATS_RECEIVED_SHOWSCHEMA_NAME_COMMAND received command to show statistics schema for %1
-The stats module received a command to show the specified statistics schema of the specified module.
-
-% STATS_START_ERROR stats module error: %1
-An internal error occurred while starting the stats module. The stats
-module will be now shutting down.

+ 1 - 0
src/bin/stats/tests/Makefile.am

@@ -24,6 +24,7 @@ endif
 	B10_FROM_SOURCE=$(abs_top_srcdir) \
 	BIND10_MSGQ_SOCKET_FILE=$(abs_top_builddir)/msgq_socket \
 	CONFIG_TESTDATA_PATH=$(abs_top_srcdir)/src/lib/config/tests/testdata \
+	B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
 	$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
 	done
 

+ 4 - 3
src/bin/tests/process_rename_test.py.in

@@ -25,7 +25,8 @@ class TestRename(unittest.TestCase):
     def __scan(self, directory, script, fun):
         # Scan one script if it contains call to the renaming function
         filename = os.path.join(directory, script)
-        data = ''.join(open(filename).readlines())
+        with open(filename) as f:
+            data = ''.join(f.readlines())
         prettyname = 'src' + filename[filename.rfind('../') + 2:]
         self.assertTrue(fun.search(data),
             "Didn't find a call to isc.util.process.rename in " + prettyname)
@@ -53,8 +54,8 @@ class TestRename(unittest.TestCase):
         # Find all Makefile and extract names of scripts
         for (d, _, fs) in os.walk('@top_builddir@'):
             if 'Makefile' in fs:
-                makefile = ''.join(open(os.path.join(d,
-                    "Makefile")).readlines())
+                with open(os.path.join(d, "Makefile")) as f:
+                    makefile = ''.join(f.readlines())
                 for (var, _) in lines.findall(re.sub(excluded_lines, '',
                                                      makefile)):
                     for (script, _) in scripts.findall(var):

+ 188 - 2
src/bin/xfrin/tests/xfrin_test.py

@@ -2127,7 +2127,8 @@ class TestXfrin(unittest.TestCase):
         self.assertFalse(self.xfr._module_cc.stopped);
         self.xfr.shutdown()
         self.assertTrue(self.xfr._module_cc.stopped);
-        sys.stderr= self.stderr_backup
+        sys.stderr.close()
+        sys.stderr = self.stderr_backup
 
     def _do_parse_zone_name_class(self):
         return self.xfr._parse_zone_name_and_class(self.args)
@@ -2577,7 +2578,7 @@ class TestXfrin(unittest.TestCase):
         self.common_ixfr_setup('refresh', False)
         self.assertEqual(RRType.AXFR(), self.xfr.xfrin_started_request_type)
 
-class TextXfrinMemoryZones(unittest.TestCase):
+class TestXfrinMemoryZones(unittest.TestCase):
     def setUp(self):
         self.xfr = MockXfrin()
         # Configuration snippet containing 2 memory datasources,
@@ -2736,6 +2737,44 @@ class TestMain(unittest.TestCase):
         MockXfrin.check_command_hook = raise_exception
         main(MockXfrin, False)
 
+class TestXfrinProcessMockCC:
+    def __init__(self):
+        self.get_called = False
+        self.get_called_correctly = False
+        self.config = []
+
+    def get_remote_config_value(self, module, identifier):
+        self.get_called = True
+        if module == 'Auth' and identifier == 'datasources':
+            self.get_called_correctly = True
+            return (self.config, False)
+        else:
+            return (None, True)
+
+class TestXfrinProcessMockCCSession:
+    def __init__(self):
+        self.send_called = False
+        self.send_called_correctly = False
+        self.recv_called = False
+        self.recv_called_correctly = False
+
+    def group_sendmsg(self, msg, module):
+        self.send_called = True
+        if module == 'Auth' and msg['command'][0] == 'loadzone':
+            self.send_called_correctly = True
+            seq = "random-e068c2de26d760f20cf10afc4b87ef0f"
+        else:
+            seq = None
+
+        return seq
+
+    def group_recvmsg(self, message, seq):
+        self.recv_called = True
+        if message == False and seq == "random-e068c2de26d760f20cf10afc4b87ef0f":
+            self.recv_called_correctly = True
+        # return values are ignored
+        return (None, None)
+
 class TestXfrinProcess(unittest.TestCase):
     """
     Some tests for the xfrin_process function. This replaces the
@@ -2751,6 +2790,8 @@ class TestXfrinProcess(unittest.TestCase):
 
         Also sets up several internal variables to watch what happens.
         """
+        self._module_cc = TestXfrinProcessMockCC()
+        self._send_cc_session = TestXfrinProcessMockCCSession()
         # This will hold a "log" of what transfers were attempted.
         self.__transfers = []
         # This will "log" if failures or successes happened.
@@ -2795,6 +2836,9 @@ class TestXfrinProcess(unittest.TestCase):
         Part of pretending to be the server as well. This just logs the
         success/failure of the previous operation.
         """
+        if ret == XFRIN_OK:
+            xfrin._do_auth_loadzone(self, zone_name, rrclass)
+
         self.__published.append(ret)
 
     def close(self):
@@ -2825,12 +2869,22 @@ class TestXfrinProcess(unittest.TestCase):
         # Create a connection for each attempt
         self.assertEqual(len(transfers), self.__created_connections)
         self.assertEqual([published], self.__published)
+        if published == XFRIN_OK:
+            self.assertTrue(self._module_cc.get_called)
+            self.assertTrue(self._module_cc.get_called_correctly)
+        else:
+            self.assertFalse(self._module_cc.get_called)
+            self.assertFalse(self._module_cc.get_called_correctly)
 
     def test_ixfr_ok(self):
         """
         Everything OK the first time, over IXFR.
         """
         self.__do_test([XFRIN_OK], [RRType.IXFR()], RRType.IXFR())
+        self.assertFalse(self._send_cc_session.send_called)
+        self.assertFalse(self._send_cc_session.send_called_correctly)
+        self.assertFalse(self._send_cc_session.recv_called)
+        self.assertFalse(self._send_cc_session.recv_called_correctly)
 
     def test_axfr_ok(self):
         """
@@ -2861,6 +2915,138 @@ class TestXfrinProcess(unittest.TestCase):
         """
         self.__do_test([XFRIN_FAIL, XFRIN_FAIL],
                        [RRType.IXFR(), RRType.AXFR()], RRType.IXFR())
+
+    def test_inmem_ok(self):
+        """
+        Inmem configuration where all the configuration is just right
+        for loadzone to be sent to b10-auth (origin is the name received
+        by xfrin, filetype is sqlite3, type is memory and class is the
+        one received by xfrin).
+        """
+        self._module_cc.config = [{'zones': [{'origin': 'example.org', 'filetype': 'sqlite3',
+                                              'file': 'data/inmem-xfrin.sqlite3'}],
+                                   'type': 'memory', 'class': 'IN'}]
+        self.__do_test([XFRIN_OK], [RRType.IXFR()], RRType.IXFR())
+        self.assertTrue(self._send_cc_session.send_called)
+        self.assertTrue(self._send_cc_session.send_called_correctly)
+        self.assertTrue(self._send_cc_session.recv_called)
+        self.assertTrue(self._send_cc_session.recv_called_correctly)
+
+    def test_inmem_datasource_type_not_memory(self):
+        """
+        Inmem configuration where the datasource type is not memory. In
+        this case, loadzone should not be sent to b10-auth.
+        """
+        self._module_cc.config = [{'zones': [{'origin': 'example.org', 'filetype': 'sqlite3',
+                                              'file': 'data/inmem-xfrin.sqlite3'}],
+                                   'type': 'punched-card', 'class': 'IN'}]
+        self.__do_test([XFRIN_OK], [RRType.IXFR()], RRType.IXFR())
+        self.assertFalse(self._send_cc_session.send_called)
+        self.assertFalse(self._send_cc_session.send_called_correctly)
+        self.assertFalse(self._send_cc_session.recv_called)
+        self.assertFalse(self._send_cc_session.recv_called_correctly)
+
+    def test_inmem_datasource_type_is_missing(self):
+        """
+        Inmem configuration where the datasource type is missing. In
+        this case, loadzone should not be sent to b10-auth.
+        """
+        self._module_cc.config = [{'zones': [{'origin': 'example.org', 'filetype': 'sqlite3',
+                                              'file': 'data/inmem-xfrin.sqlite3'}],
+                                   'class': 'IN'}]
+        self.__do_test([XFRIN_OK], [RRType.IXFR()], RRType.IXFR())
+        self.assertFalse(self._send_cc_session.send_called)
+        self.assertFalse(self._send_cc_session.send_called_correctly)
+        self.assertFalse(self._send_cc_session.recv_called)
+        self.assertFalse(self._send_cc_session.recv_called_correctly)
+
+    def test_inmem_backend_type_not_sqlite3(self):
+        """
+        Inmem configuration where the datasource backing file is not of
+        type sqlite3. In this case, loadzone should not be sent to
+        b10-auth.
+        """
+        self._module_cc.config = [{'zones': [{'origin': 'example.org', 'filetype': 'postgresql',
+                                              'file': 'data/inmem-xfrin.db'}],
+                                   'type': 'memory', 'class': 'IN'}]
+        self.__do_test([XFRIN_OK], [RRType.IXFR()], RRType.IXFR())
+        self.assertFalse(self._send_cc_session.send_called)
+        self.assertFalse(self._send_cc_session.send_called_correctly)
+        self.assertFalse(self._send_cc_session.recv_called)
+        self.assertFalse(self._send_cc_session.recv_called_correctly)
+
+    def test_inmem_backend_type_is_missing(self):
+        """
+        Inmem configuration where the datasource backing file type is
+        not set. In this case, loadzone should not be sent to b10-auth.
+        """
+        self._module_cc.config = [{'zones': [{'origin': 'example.org',
+                                              'file': 'data/inmem-xfrin'}],
+                                   'type': 'memory', 'class': 'IN'}]
+        self.__do_test([XFRIN_OK], [RRType.IXFR()], RRType.IXFR())
+        self.assertFalse(self._send_cc_session.send_called)
+        self.assertFalse(self._send_cc_session.send_called_correctly)
+        self.assertFalse(self._send_cc_session.recv_called)
+        self.assertFalse(self._send_cc_session.recv_called_correctly)
+
+    def test_inmem_class_is_different(self):
+        """
+        Inmem configuration where the datasource class does not match
+        the received class. In this case, loadzone should not be sent to
+        b10-auth.
+        """
+        self._module_cc.config = [{'zones': [{'origin': 'example.org', 'filetype': 'sqlite3',
+                                              'file': 'data/inmem-xfrin.sqlite3'}],
+                                   'type': 'memory', 'class': 'XX'}]
+        self.__do_test([XFRIN_OK], [RRType.IXFR()], RRType.IXFR())
+        self.assertFalse(self._send_cc_session.send_called)
+        self.assertFalse(self._send_cc_session.send_called_correctly)
+        self.assertFalse(self._send_cc_session.recv_called)
+        self.assertFalse(self._send_cc_session.recv_called_correctly)
+
+    def test_inmem_class_is_missing(self):
+        """
+        Inmem configuration where the datasource class is missing. In
+        this case, we assume the IN class and loadzone may be sent to
+        b10-auth if everything else matches.
+        """
+        self._module_cc.config = [{'zones': [{'origin': 'example.org', 'filetype': 'sqlite3',
+                                              'file': 'data/inmem-xfrin.sqlite3'}],
+                                   'type': 'memory'}]
+        self.__do_test([XFRIN_OK], [RRType.IXFR()], RRType.IXFR())
+        self.assertTrue(self._send_cc_session.send_called)
+        self.assertTrue(self._send_cc_session.send_called_correctly)
+        self.assertTrue(self._send_cc_session.recv_called)
+        self.assertTrue(self._send_cc_session.recv_called_correctly)
+
+    def test_inmem_name_doesnt_match(self):
+        """
+        Inmem configuration where the origin does not match the received
+        name. In this case, loadzone should not be sent to b10-auth.
+        """
+        self._module_cc.config = [{'zones': [{'origin': 'isc.org', 'filetype': 'sqlite3',
+                                              'file': 'data/inmem-xfrin.sqlite3'}],
+                                   'type': 'memory', 'class': 'IN'}]
+        self.__do_test([XFRIN_OK], [RRType.IXFR()], RRType.IXFR())
+        self.assertFalse(self._send_cc_session.send_called)
+        self.assertFalse(self._send_cc_session.send_called_correctly)
+        self.assertFalse(self._send_cc_session.recv_called)
+        self.assertFalse(self._send_cc_session.recv_called_correctly)
+
+    def test_inmem_name_is_missing(self):
+        """
+        Inmem configuration where the origin is missing. In this case,
+        loadzone should not be sent to b10-auth.
+        """
+        self._module_cc.config = [{'zones': [{'filetype': 'sqlite3',
+                                              'file': 'data/inmem-xfrin.sqlite3'}],
+                                   'type': 'memory', 'class': 'IN'}]
+        self.__do_test([XFRIN_OK], [RRType.IXFR()], RRType.IXFR())
+        self.assertFalse(self._send_cc_session.send_called)
+        self.assertFalse(self._send_cc_session.send_called_correctly)
+        self.assertFalse(self._send_cc_session.recv_called)
+        self.assertFalse(self._send_cc_session.recv_called_correctly)
+
 class TestFormatting(unittest.TestCase):
     # If the formatting functions are moved to a more general library
     # (ticket #1379), these tests should be moved with them.

+ 16 - 4
src/bin/xfrin/xfrin.py.in

@@ -33,6 +33,7 @@ import isc.util.process
 from isc.datasrc import DataSourceClient, ZoneFinder
 import isc.net.parse
 from isc.xfrin.diff import Diff
+from isc.server_common.auth_command import auth_loadzone_command
 from isc.log_messages.xfrin_messages import *
 
 isc.log.init("b10-xfrin")
@@ -66,10 +67,10 @@ else:
 SPECFILE_LOCATION = SPECFILE_PATH + "/xfrin.spec"
 AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + "/auth.spec"
 
+AUTH_MODULE_NAME = 'Auth'
 XFROUT_MODULE_NAME = 'Xfrout'
 ZONE_MANAGER_MODULE_NAME = 'Zonemgr'
 REFRESH_FROM_ZONEMGR = 'refresh_from_zonemgr'
-ZONE_XFRIN_FAILED = 'zone_xfrin_failed'
 
 # Constants for debug levels.
 DBG_XFRIN_TRACE = logger.DBGLVL_TRACE_BASIC
@@ -1246,6 +1247,15 @@ class ZoneInfo:
         return (self.master_addr.family, socket.SOCK_STREAM,
                 (str(self.master_addr), self.master_port))
 
+def _do_auth_loadzone(server, zone_name, zone_class):
+    msg = auth_loadzone_command(server._module_cc, zone_name, zone_class)
+    if msg is not None:
+        param = msg['command'][1]
+        logger.debug(DBG_XFRIN_TRACE, XFRIN_AUTH_LOADZONE, param["origin"],
+                     param["class"], param["datasrc"])
+        seq = server._send_cc_session.group_sendmsg(msg, AUTH_MODULE_NAME)
+        answer, env = server._send_cc_session.group_recvmsg(False, seq)
+
 class Xfrin:
     def __init__(self):
         self._max_transfers_in = 10
@@ -1529,7 +1539,7 @@ class Xfrin:
 
     def _set_db_file(self):
         db_file, is_default =\
-            self._module_cc.get_remote_config_value("Auth", "database_file")
+            self._module_cc.get_remote_config_value(AUTH_MODULE_NAME, "database_file")
         if is_default and "B10_FROM_BUILD" in os.environ:
             # override the local database setting if it is default and we
             # are running from the source tree
@@ -1539,7 +1549,7 @@ class Xfrin:
                       "bind10_zones.sqlite3"
         self._db_file = db_file
 
-    def publish_xfrin_news(self, zone_name, zone_class,  xfr_result):
+    def publish_xfrin_news(self, zone_name, zone_class, xfr_result):
         '''Send command to xfrout/zone manager module.
         If xfrin has finished successfully for one zone, tell the good
         news(command: zone_new_data_ready) to zone manager and xfrout.
@@ -1548,6 +1558,7 @@ class Xfrin:
         param = {'zone_name': zone_name.to_text(),
                  'zone_class': zone_class.to_text()}
         if xfr_result == XFRIN_OK:
+            _do_auth_loadzone(self, zone_name, zone_class)
             msg = create_command(notify_out.ZONE_NEW_DATA_READY_CMD, param)
             # catch the exception, in case msgq has been killed.
             try:
@@ -1566,8 +1577,9 @@ class Xfrin:
                     pass        # for now we just ignore the failure
             except socket.error as err:
                 logger.error(XFRIN_MSGQ_SEND_ERROR, XFROUT_MODULE_NAME, ZONE_MANAGER_MODULE_NAME)
+
         else:
-            msg = create_command(ZONE_XFRIN_FAILED, param)
+            msg = create_command(notify_out.ZONE_XFRIN_FAILED, param)
             # catch the exception, in case msgq has been killed.
             try:
                 seq = self._send_cc_session.group_sendmsg(msg, ZONE_MANAGER_MODULE_NAME)

+ 9 - 0
src/bin/xfrin/xfrin_messages.mes

@@ -15,6 +15,11 @@
 # No namespace declaration - these constants go in the global namespace
 # of the xfrin messages python module.
 
+% XFRIN_AUTH_LOADZONE sending Auth loadzone for origin=%1, class=%2, datasrc=%3
+There was a successful zone transfer, and the zone is served by b10-auth
+in the in-memory data source using sqlite3 as a backend. We send the
+"loadzone" command for the zone to b10-auth.
+
 % XFRIN_AXFR_INCONSISTENT_SOA AXFR SOAs are inconsistent for %1: %2 expected, %3 received
 The serial fields of the first and last SOAs of AXFR (including AXFR-style
 IXFR) are not the same.  According to RFC 5936 these two SOAs must be the
@@ -113,6 +118,10 @@ There was a problem sending a message to the xfrout module or the
 zone manager. This most likely means that the msgq daemon has quit or
 was killed.
 
+% XFRIN_MSGQ_SEND_ERROR_AUTH error while contacting %1
+There was a problem sending a message to b10-auth. This most likely
+means that the msgq daemon has quit or was killed.
+
 % XFRIN_MSGQ_SEND_ERROR_ZONE_MANAGER error while contacting %1
 There was a problem sending a message to the zone manager. This most
 likely means that the msgq daemon has quit or was killed.

+ 52 - 8
src/bin/xfrout/tests/xfrout_test.py.in

@@ -60,6 +60,9 @@ class MySocket():
         self.sendqueue.extend(data);
         return len(data)
 
+    def fileno(self):
+        return 42               # simply return a constant dummy value
+
     def readsent(self):
         if len(self.sendqueue) >= 2:
             size = 2 + struct.unpack("!H", self.sendqueue[:2])[0]
@@ -1155,6 +1158,15 @@ class TestUnixSockServer(unittest.TestCase):
     def setUp(self):
         self.write_sock, self.read_sock = socket.socketpair()
         self.unix = MyUnixSockServer()
+        # Some test below modify these module-wide attributes.  We'll need
+        # to restore them at the end of each test, so we remember them here.
+        self.__select_bak = xfrout.select.select
+        self.__recv_fd_back = xfrout.recv_fd
+
+    def tearDown(self):
+        # Restore possibly faked module-wide attributes.
+        xfrout.select.select = self.__select_bak
+        xfrout.recv_fd = self.__recv_fd_back
 
     def test_tsig_keyring(self):
         """
@@ -1201,6 +1213,7 @@ class TestUnixSockServer(unittest.TestCase):
         self.assertEqual((socket.AF_INET, socket.SOCK_STREAM,
                           ('127.0.0.1', 12345)),
                          self.unix._guess_remote(sock.fileno()))
+        sock.close()
         if socket.has_ipv6:
             # Don't check IPv6 address on hosts not supporting them
             sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
@@ -1208,6 +1221,7 @@ class TestUnixSockServer(unittest.TestCase):
             self.assertEqual((socket.AF_INET6, socket.SOCK_STREAM,
                               ('::1', 12345, 0, 0)),
                              self.unix._guess_remote(sock.fileno()))
+            sock.close()
             # Try when pretending there's no IPv6 support
             # (No need to pretend when there's really no IPv6)
             xfrout.socket.has_ipv6 = False
@@ -1216,6 +1230,7 @@ class TestUnixSockServer(unittest.TestCase):
             self.assertEqual((socket.AF_INET, socket.SOCK_STREAM,
                               ('127.0.0.1', 12345)),
                              self.unix._guess_remote(sock.fileno()))
+            sock.close()
             # Return it back
             xfrout.socket.has_ipv6 = True
 
@@ -1375,19 +1390,13 @@ class TestUnixSockServer(unittest.TestCase):
         self._remove_file(sock_file)
         self.assertFalse(self.unix._sock_file_in_use(sock_file))
         self._start_unix_sock_server(sock_file)
-
-        old_stdout = sys.stdout
-        sys.stdout = open(os.devnull, 'w')
         self.assertTrue(self.unix._sock_file_in_use(sock_file))
-        sys.stdout = old_stdout
 
     def test_remove_unused_sock_file_in_use(self):
         sock_file = 'temp.sock.file'
         self._remove_file(sock_file)
         self.assertFalse(self.unix._sock_file_in_use(sock_file))
         self._start_unix_sock_server(sock_file)
-        old_stdout = sys.stdout
-        sys.stdout = open(os.devnull, 'w')
         try:
             self.unix._remove_unused_sock_file(sock_file)
         except SystemExit:
@@ -1396,8 +1405,6 @@ class TestUnixSockServer(unittest.TestCase):
             # This should never happen
             self.assertTrue(False)
 
-        sys.stdout = old_stdout
-
     def test_remove_unused_sock_file_dir(self):
         import tempfile
         dir_name = tempfile.mkdtemp()
@@ -1411,9 +1418,46 @@ class TestUnixSockServer(unittest.TestCase):
             # This should never happen
             self.assertTrue(False)
 
+        sys.stdout.close()
         sys.stdout = old_stdout
         os.rmdir(dir_name)
 
+    def __fake_select(self, r, w, e):
+        '''select emulator used in select_loop_fail test.'''
+        # This simplified faked function assumes to be called at most once,
+        # and in that case just return a pre-configured "readable" sockets.
+        if self.__select_count > 0:
+            raise RuntimeError('select called unexpected number of times')
+        self.__select_count += 1
+        return (self.__select_return_redable, [], [])
+
+    def test_select_loop_fail(self):
+        '''Check failure events in the main loop.'''
+        # setup faked select() environments
+        self.unix._read_sock = MySocket(socket.AF_INET6, socket.SOCK_STREAM)
+        xfrout.select.select = self.__fake_select
+        self.__select_return_redable = [MySocket(socket.AF_INET6,
+                                                 socket.SOCK_STREAM)]
+
+        # Check that loop terminates if recv_fd() fails.
+        for ret_code in [-1, FD_SYSTEM_ERROR]:
+            # fake recv_fd so it returns the faked failure code.
+            xfrout.recv_fd = lambda fileno: ret_code
+
+            # reset the counter, go to the loop.
+            self.__select_count = 0
+            self.unix._select_loop(self.__select_return_redable[0])
+            # select should have been called exactly once.
+            self.assertEqual(1, self.__select_count)
+
+        # Next, we test the case where recf_fd succeeds but receiving the
+        # request fails.
+        self.__select_count = 0
+        xfrout.recv_fd = lambda fileno: 1
+        self.unix._receive_query_message = lambda fd: None
+        self.unix._select_loop(self.__select_return_redable[0])
+        self.assertEqual(1, self.__select_count)
+
 class TestInitialization(unittest.TestCase):
     def setEnv(self, name, value):
         if value is None:

+ 43 - 20
src/bin/xfrout/xfrout.py.in

@@ -678,30 +678,40 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
         except socket.error:
             logger.error(XFROUT_FETCH_REQUEST_ERROR)
             return
+        self._select_loop(request)
+
+    def _select_loop(self, request_sock):
+        '''Main loop for a single session between xfrout and auth.
+
+        This is a dedicated subroutine of handle_request(), but is defined
+        as a separate "protected" method for the convenience of tests.
+        '''
 
         # Check self._shutdown_event to ensure the real shutdown comes.
         # Linux could trigger a spurious readable event on the _read_sock
         # due to a bug, so we need perform a double check.
         while not self._shutdown_event.is_set(): # Check if xfrout is shutdown
             try:
-                (rlist, wlist, xlist) = select.select([self._read_sock, request], [], [])
+                (rlist, wlist, xlist) = select.select([self._read_sock,
+                                                       request_sock], [], [])
             except select.error as e:
                 if e.args[0] == errno.EINTR:
                     (rlist, wlist, xlist) = ([], [], [])
                     continue
                 else:
-                    logger.error(XFROUT_SOCKET_SELECT_ERROR, str(e))
+                    logger.error(XFROUT_SOCKET_SELECT_ERROR, e)
                     break
 
-            # self.server._shutdown_event will be set by now, if it is not a false
-            # alarm
+            # self.server._shutdown_event will be set by now, if it is not a
+            # false alarm
             if self._read_sock in rlist:
                 continue
 
             try:
-                self.process_request(request)
+                if not self.process_request(request_sock):
+                    break
             except Exception as pre:
-                logger.error(XFROUT_PROCESS_REQUEST_ERROR, str(pre))
+                logger.error(XFROUT_PROCESS_REQUEST_ERROR, pre)
                 break
 
     def _handle_request_noblock(self):
@@ -713,26 +723,33 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
 
     def process_request(self, request):
         """Receive socket fd and query message from auth, then
-        start a new thread to process the request."""
+        start a new thread to process the request.
+
+        Return: True if everything is okay; otherwise False, in which case
+        the calling thread will terminate.
+
+        """
         sock_fd = recv_fd(request.fileno())
         if sock_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.
-            if sock_fd == FD_SYSTEM_ERROR:
-                logger.error(XFROUT_RECEIVE_FILE_DESCRIPTOR_ERROR)
-            return
+            logger.warn(XFROUT_RECEIVE_FILE_DESCRIPTOR_ERROR)
+            return False
 
-        # receive request msg
+        # receive request msg.  If it fails we simply terminate the thread;
+        # it might be possible to recover from this state, but it's more likely
+        # that auth and xfrout are in inconsistent states.  So it will make
+        # more sense to restart in a new session.
         request_data = self._receive_query_message(request)
-        if not request_data:
-            return
+        if request_data is None:
+            # The specific exception type doesn't matter so we use session
+            # error.
+            raise XfroutSessionError('Failed to get complete xfr request')
 
         t = threading.Thread(target=self.finish_request,
-                             args = (sock_fd, request_data))
+                             args=(sock_fd, request_data))
         if self.daemon_threads:
             t.daemon = True
         t.start()
+        return True
 
     def _guess_remote(self, sock_fd):
         """Guess remote address and port of the socket.
@@ -747,12 +764,15 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
         # to care about the SOCK_STREAM parameter at all (which it really is,
         # except for testing)
         if socket.has_ipv6:
-            sock = socket.fromfd(sock_fd, socket.AF_INET6, socket.SOCK_STREAM)
+            sock_domain = socket.AF_INET6
         else:
             # To make it work even on hosts without IPv6 support
             # (Any idea how to simulate this in test?)
-            sock = socket.fromfd(sock_fd, socket.AF_INET, socket.SOCK_STREAM)
+            sock_domain = socket.AF_INET
+
+        sock = socket.fromfd(sock_fd, sock_domain, socket.SOCK_STREAM)
         peer = sock.getpeername()
+        sock.close()
 
         # Identify the correct socket family.  Due to the above "trick",
         # we cannot simply use sock.family.
@@ -761,6 +781,7 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
             socket.inet_pton(socket.AF_INET6, peer[0])
         except socket.error:
             family = socket.AF_INET
+
         return (family, socket.SOCK_STREAM, peer)
 
     def finish_request(self, sock_fd, request_data):
@@ -805,8 +826,10 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
             sock = socket.socket(socket.AF_UNIX)
             sock.connect(sock_file)
         except socket.error as err:
+            sock.close()
             return False
         else:
+            sock.close()
             return True
 
     def shutdown(self):
@@ -985,7 +1008,7 @@ class XfroutServer:
             self.shutdown()
             answer = create_answer(0)
 
-        elif cmd == notify_out.ZONE_NEW_DATA_READY_CMD:
+        elif cmd == "notify":
             zone_name = args.get('zone_name')
             zone_class = args.get('zone_class')
             if not zone_class:

+ 88 - 77
src/bin/xfrout/xfrout_messages.mes

@@ -23,22 +23,15 @@ a valid TSIG key.
 There was a problem reading from the command and control channel. The
 most likely cause is that the msgq daemon is not running.
 
-% XFROUT_MODULECC_SESSION_ERROR error encountered by configuration/command module: %1
-There was a problem in the lower level module handling configuration and
-control commands.  This could happen for various reasons, but the most likely
-cause is that the configuration database contains a syntax error and xfrout
-failed to start at initialization.  A detailed error message from the module
-will also be displayed.
-
-% XFROUT_CONFIG_ERROR error found in configuration data: %1
-The xfrout process encountered an error when installing the configuration at
-startup time.  Details of the error are included in the log message.
-
 % XFROUT_CC_SESSION_TIMEOUT_ERROR timeout waiting for cc response
 There was a problem reading a response from another module over the
 command and control channel. The most likely cause is that the
 configuration manager b10-cfgmgr is not running.
 
+% XFROUT_CONFIG_ERROR error found in configuration data: %1
+The xfrout process encountered an error when installing the configuration at
+startup time.  Details of the error are included in the log message.
+
 % XFROUT_FETCH_REQUEST_ERROR socket error while fetching a request from the auth daemon
 There was a socket error while contacting the b10-auth daemon to
 fetch a transfer request. The auth daemon may have shutdown.
@@ -56,6 +49,52 @@ are missing on the system, or the PYTHONPATH variable is not correct.
 The specific place where this library needs to be depends on your
 system and your specific installation.
 
+% XFROUT_IXFR_MULTIPLE_SOA IXFR client %1: authority section has multiple SOAs
+An IXFR request was received with more than one SOA RRs in the authority
+section.  The xfrout daemon rejects the request with an RCODE of
+FORMERR.
+
+% XFROUT_IXFR_NO_JOURNAL_SUPPORT IXFR client %1, %2: journaling not supported in the data source, falling back to AXFR
+An IXFR request was received but the underlying data source did
+not support journaling.  The xfrout daemon fell back to AXFR-style
+IXFR.
+
+% XFROUT_IXFR_NO_SOA IXFR client %1: missing SOA
+An IXFR request was received with no SOA RR in the authority section.
+The xfrout daemon rejects the request with an RCODE of FORMERR.
+
+% XFROUT_IXFR_NO_VERSION IXFR client %1, %2: version (%3 to %4) not in journal, falling back to AXFR
+An IXFR request was received, but the requested range of differences
+were not found in the data source.  The xfrout daemon fell back to
+AXFR-style IXFR.
+
+% XFROUT_IXFR_NO_ZONE IXFR client %1, %2: zone not found with journal
+The requested zone in IXFR was not found in the data source
+even though the xfrout daemon sucessfully found the SOA RR of the zone
+in the data source.  This can happen if the administrator removed the
+zone from the data source within the small duration between these
+operations, but it's more likely to be a bug or broken data source.
+Unless you know why this message was logged, and especially if it
+happens often, it's advisable to check whether the data source is
+valid for this zone.  The xfrout daemon considers it a possible,
+though unlikely, event, and returns a response with an RCODE of
+NOTAUTH.
+
+% XFROUT_IXFR_UPTODATE IXFR client %1, %2: client version is new enough (theirs=%3, ours=%4)
+An IXFR request was received, but the client's SOA version is the same as
+or newer than that of the server.  The xfrout server responds to the
+request with the answer section being just one SOA of that version.
+Note: as of this wrting the 'newer version' cannot be identified due to
+the lack of support for the serial number arithmetic.  This will soon
+be implemented.
+
+% XFROUT_MODULECC_SESSION_ERROR error encountered by configuration/command module: %1
+There was a problem in the lower level module handling configuration and
+control commands.  This could happen for various reasons, but the most likely
+cause is that the configuration database contains a syntax error and xfrout
+failed to start at initialization.  A detailed error message from the module
+will also be displayed.
+
 % XFROUT_NEW_CONFIG Update xfrout configuration
 New configuration settings have been sent from the configuration
 manager. The xfrout daemon will now apply them.
@@ -76,11 +115,15 @@ In general, this should only occur for unexpected problems like
 memory allocation failures, as the query should already have been
 parsed by the b10-auth daemon, before it was passed here.
 
-% XFROUT_PROCESS_REQUEST_ERROR error processing transfer request: %2
-There was an error processing a transfer request. The error is included
-in the log message, but at this point no specific information other
-than that could be given. This points to incomplete exception handling
-in the code.
+% XFROUT_PROCESS_REQUEST_ERROR error processing transfer request: %1
+There was an error in receiving a transfer request from b10-auth.
+This is generally an unexpected event, but is possible when, for
+example, b10-auth terminates in the middle of forwarding the request.
+When this happens it's unlikely to be recoverable with the same
+communication session with b10-auth, so b10-xfrout drops it and
+waits for a new session.  In any case, this error indicates that
+there's something very wrong in the system, so it's advisable to check
+the over all status of the BIND 10 system.
 
 % XFROUT_QUERY_DROPPED %1 client %2: request to transfer %3 dropped
 The xfrout process silently dropped a request to transfer zone to
@@ -88,12 +131,6 @@ given host.  This is required by the ACLs.  The %2 represents the IP
 address and port of the peer requesting the transfer, and the %3
 represents the zone name and class.
 
-% XFROUT_QUERY_REJECTED %1 client %2: request to transfer %3 rejected
-The xfrout process rejected (by REFUSED rcode) a request to transfer zone to
-given host. This is because of ACLs.  The %2 represents the IP
-address and port of the peer requesting the transfer, and the %3
-represents the zone name and class.
-
 % XFROUT_QUERY_QUOTA_EXCCEEDED %1 client %2: request denied due to quota (%3)
 The xfr request was rejected because the server was already handling
 the maximum number of allowable transfers as specified in the transfers_out
@@ -104,20 +141,28 @@ this parameter; if the server is being too busy due to requests from
 unexpected clients you may want to restrict the legitimate clients
 with ACL.
 
-% XFROUT_RECEIVE_FILE_DESCRIPTOR_ERROR error receiving the file descriptor for an XFR connection
-There was an error receiving the file descriptor for the transfer
-request. Normally, the request is received by b10-auth, and passed on
-to the xfrout daemon, so it can answer directly. However, there was a
-problem receiving this file descriptor. The request will be ignored.
+% XFROUT_QUERY_REJECTED %1 client %2: request to transfer %3 rejected
+The xfrout process rejected (by REFUSED rcode) a request to transfer zone to
+given host. This is because of ACLs.  The %2 represents the IP
+address and port of the peer requesting the transfer, and the %3
+represents the zone name and class.
 
 % XFROUT_RECEIVED_SHUTDOWN_COMMAND shutdown command received
 The xfrout daemon received a shutdown command from the command channel
 and will now shut down.
 
-% XFROUT_REMOVE_UNIX_SOCKET_FILE_ERROR error clearing unix socket file %1: %2
-When shutting down, the xfrout daemon tried to clear the unix socket
-file used for communication with the auth daemon. It failed to remove
-the file. The reason for the failure is given in the error message.
+% XFROUT_RECEIVE_FILE_DESCRIPTOR_ERROR error receiving the file descriptor for an XFR connection
+There was an error receiving the file descriptor for the transfer
+request from b10-auth.  There can be several reasons for this, but
+the most likely cause is that b10-auth terminates for some reason
+(maybe it's a bug of b10-auth, maybe it's an intentional restart by
+the administrator), so depending on how this happens it may or may not
+be a serious error.  But in any case this is not expected to happen
+frequently, and it's advisable to figure out how this happened if
+this message is logged.  Even if this error happens xfrout will reset
+its internal state and will keep receiving further requests.  So
+if it's just a temporary restart of b10-auth the administrator does
+not have to do anything.
 
 % XFROUT_REMOVE_OLD_UNIX_SOCKET_FILE_ERROR error removing unix socket file %1: %2
 The unix socket file xfrout needs for contact with the auth daemon
@@ -126,6 +171,11 @@ removing it. It is likely that we do not have permission to remove
 this file. The specific error is show in the log message. The xfrout
 daemon will shut down.
 
+% XFROUT_REMOVE_UNIX_SOCKET_FILE_ERROR error clearing unix socket file %1: %2
+When shutting down, the xfrout daemon tried to clear the unix socket
+file used for communication with the auth daemon. It failed to remove
+the file. The reason for the failure is given in the error message.
+
 % XFROUT_SOCKET_SELECT_ERROR error while calling select() on request socket: %1
 There was an error while calling select() on the socket that informs
 the xfrout daemon that a new xfrout request has arrived. This should
@@ -151,6 +201,13 @@ on, but the file is in use. The most likely cause is that another
 xfrout daemon process is still running. This xfrout daemon (the one
 printing this message) will not start.
 
+% XFROUT_XFR_TRANSFER_CHECK_ERROR %1 client %2: check for transfer of %3 failed: %4
+Pre-response check for an incomding XFR request failed unexpectedly.
+The most likely cause of this is that some low level error in the data
+source, but it may also be other general (more unlikely) errors such
+as memory shortage.  Some detail of the error is also included in the
+message.  The xfrout server tries to return a SERVFAIL response in this case.
+
 % XFROUT_XFR_TRANSFER_DONE %1 client %2: transfer of %3 complete
 The transfer of the given zone has been completed successfully, or was
 aborted due to a shutdown event.
@@ -161,13 +218,6 @@ an AXFR query. The error message of the exception is included in the
 log message, but this error most likely points to incomplete exception
 handling in the code.
 
-% XFROUT_XFR_TRANSFER_CHECK_ERROR %1 client %2: check for transfer of %3 failed: %4
-Pre-response check for an incomding XFR request failed unexpectedly.
-The most likely cause of this is that some low level error in the data
-source, but it may also be other general (more unlikely) errors such
-as memory shortage.  Some detail of the error is also included in the
-message.  The xfrout server tries to return a SERVFAIL response in this case.
-
 % XFROUT_XFR_TRANSFER_FAILED %1 client %2: transfer of %3 failed, rcode: %4
 A transfer out for the given zone failed. An error response is sent
 to the client. The given rcode is the rcode that is set in the error
@@ -181,42 +231,3 @@ Xfrout/max_transfers_out, has been reached).
 
 % XFROUT_XFR_TRANSFER_STARTED %1 client %2: transfer of zone %3 has started
 A transfer out of the given zone has started.
-
-% XFROUT_IXFR_MULTIPLE_SOA IXFR client %1: authority section has multiple SOAs
-An IXFR request was received with more than one SOA RRs in the authority
-section.  The xfrout daemon rejects the request with an RCODE of
-FORMERR.
-
-% XFROUT_IXFR_NO_SOA IXFR client %1: missing SOA
-An IXFR request was received with no SOA RR in the authority section.
-The xfrout daemon rejects the request with an RCODE of FORMERR.
-
-% XFROUT_IXFR_NO_JOURNAL_SUPPORT IXFR client %1, %2: journaling not supported in the data source, falling back to AXFR
-An IXFR request was received but the underlying data source did
-not support journaling.  The xfrout daemon fell back to AXFR-style
-IXFR.
-
-% XFROUT_IXFR_UPTODATE IXFR client %1, %2: client version is new enough (theirs=%3, ours=%4)
-An IXFR request was received, but the client's SOA version is the same as
-or newer than that of the server.  The xfrout server responds to the
-request with the answer section being just one SOA of that version.
-Note: as of this wrting the 'newer version' cannot be identified due to
-the lack of support for the serial number arithmetic.  This will soon
-be implemented.
-
-% XFROUT_IXFR_NO_VERSION IXFR client %1, %2: version (%3 to %4) not in journal, falling back to AXFR
-An IXFR request was received, but the requested range of differences
-were not found in the data source.  The xfrout daemon fell back to
-AXFR-style IXFR.
-
-% XFROUT_IXFR_NO_ZONE IXFR client %1, %2: zone not found with journal
-The requested zone in IXFR was not found in the data source
-even though the xfrout daemon sucessfully found the SOA RR of the zone
-in the data source.  This can happen if the administrator removed the
-zone from the data source within the small duration between these
-operations, but it's more likely to be a bug or broken data source.
-Unless you know why this message was logged, and especially if it
-happens often, it's advisable to check whether the data source is
-valid for this zone.  The xfrout daemon considers it a possible,
-though unlikely, event, and returns a response with an RCODE of
-NOTAUTH.

+ 3 - 1
src/bin/zonemgr/tests/zonemgr_test.py

@@ -21,6 +21,7 @@ import os
 import tempfile
 from zonemgr import *
 from isc.testutils.ccsession_mock import MockModuleCCSession
+from isc.notify import notify_out
 
 ZONE_NAME_CLASS1_IN = ("example.net.", "IN")
 ZONE_NAME_CLASS1_CH = ("example.net.", "CH")
@@ -111,6 +112,7 @@ class TestZonemgrRefresh(unittest.TestCase):
     def tearDown(self):
         if os.path.exists(TEST_SQLITE3_DBFILE):
             os.unlink(TEST_SQLITE3_DBFILE)
+        sys.stderr.close()
         sys.stderr = self.stderr_backup
 
     def test_random_jitter(self):
@@ -683,7 +685,7 @@ class TestZonemgr(unittest.TestCase):
         self.assertEqual(answer1, self.zonemgr._parse_cmd_params(params1, ZONE_NOTIFY_COMMAND))
         params2 = {"zone_name" : "example.com.", "zone_class" : "IN"}
         answer2 = ZONE_NAME_CLASS3_IN
-        self.assertEqual(answer2, self.zonemgr._parse_cmd_params(params2, ZONE_XFRIN_SUCCESS_COMMAND))
+        self.assertEqual(answer2, self.zonemgr._parse_cmd_params(params2, notify_out.ZONE_NEW_DATA_READY_CMD))
         self.assertRaises(ZonemgrException, self.zonemgr._parse_cmd_params, params2, ZONE_NOTIFY_COMMAND)
         params1 = {"zone_class" : "CH"}
         self.assertRaises(ZonemgrException, self.zonemgr._parse_cmd_params, params2, ZONE_NOTIFY_COMMAND)

+ 6 - 5
src/bin/zonemgr/zonemgr.py.in

@@ -39,6 +39,7 @@ from optparse import OptionParser, OptionValueError
 from isc.config.ccsession import *
 import isc.util.process
 from isc.log_messages.zonemgr_messages import *
+from isc.notify import notify_out
 
 # Initialize logging for called modules.
 isc.log.init("b10-zonemgr")
@@ -78,8 +79,6 @@ XFRIN_MODULE_NAME = 'Xfrin'
 AUTH_MODULE_NAME = 'Auth'
 
 # define command name
-ZONE_XFRIN_FAILED_COMMAND = 'zone_xfrin_failed'
-ZONE_XFRIN_SUCCESS_COMMAND = 'zone_new_data_ready'
 ZONE_REFRESH_COMMAND = 'refresh_from_zonemgr'
 ZONE_NOTIFY_COMMAND = 'notify'
 
@@ -428,6 +427,8 @@ class ZonemgrRefresh:
         self._thread.join()
         # Wipe out what we do not need
         self._thread = None
+        self._read_sock.close()
+        self._write_sock.close()
         self._read_sock = None
         self._write_sock = None
 
@@ -621,7 +622,7 @@ class Zonemgr:
     def command_handler(self, command, args):
         """Handle command receivd from command channel.
         ZONE_NOTIFY_COMMAND is issued by Auth process;
-        ZONE_XFRIN_SUCCESS_COMMAND and ZONE_XFRIN_FAILED_COMMAND are issued by
+        ZONE_NEW_DATA_READY_CMD and ZONE_XFRIN_FAILED are issued by
         Xfrin process;
         shutdown is issued by a user or Boss process. """
         answer = create_answer(0)
@@ -635,7 +636,7 @@ class Zonemgr:
             # Send notification to zonemgr timer thread
             self._master_socket.send(b" ")# make self._slave_socket readble
 
-        elif command == ZONE_XFRIN_SUCCESS_COMMAND:
+        elif command == notify_out.ZONE_NEW_DATA_READY_CMD:
             """ Handle xfrin success command"""
             zone_name_class = self._parse_cmd_params(args, command)
             logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_SUCCESS, zone_name_class[0], zone_name_class[1])
@@ -643,7 +644,7 @@ class Zonemgr:
                 self._zone_refresh.zone_refresh_success(zone_name_class)
             self._master_socket.send(b" ")# make self._slave_socket readble
 
-        elif command == ZONE_XFRIN_FAILED_COMMAND:
+        elif command == notify_out.ZONE_XFRIN_FAILED:
             """ Handle xfrin fail command"""
             zone_name_class = self._parse_cmd_params(args, command)
             logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_FAILED, zone_name_class[0], zone_name_class[1])

+ 4 - 4
src/bin/zonemgr/zonemgr_messages.mes

@@ -67,10 +67,6 @@ zone manager to record the master server for the zone and start a timer;
 when the timer expires, the master will be polled to see if it contains
 new data.
 
-% ZONEMGR_STARTED zonemgr started
-This informational message is output by zonemgr when all initialization
-has been completed and it is entering its main loop.
-
 % ZONEMGR_RECEIVE_SHUTDOWN received SHUTDOWN command
 This is a debug message indicating that the zone manager has received
 a SHUTDOWN command over the command channel from the Boss process.
@@ -120,6 +116,10 @@ problem is that the daemon is not running.
 % ZONEMGR_SHUTDOWN zone manager has shut down
 A debug message, output when the zone manager has shut down completely.
 
+% ZONEMGR_STARTED zonemgr started
+This informational message is output by zonemgr when all initialization
+has been completed and it is entering its main loop.
+
 % ZONEMGR_STARTING zone manager starting
 A debug message output when the zone manager starts up.
 

+ 3 - 0
src/lib/acl/tests/Makefile.am

@@ -8,6 +8,9 @@ endif
 
 CLEANFILES = *.gcno *.gcda
 
+TESTS_ENVIRONMENT = \
+	libtool --mode=execute $(VALGRIND_COMMAND)
+
 TESTS =
 if HAVE_GTEST
 TESTS += run_unittests

+ 3 - 0
src/lib/asiodns/tests/Makefile.am

@@ -12,6 +12,9 @@ endif
 
 CLEANFILES = *.gcno *.gcda
 
+TESTS_ENVIRONMENT = \
+	libtool --mode=execute $(VALGRIND_COMMAND)
+
 TESTS =
 if HAVE_GTEST
 TESTS += run_unittests

+ 0 - 0
src/lib/asiolink/io_endpoint.cc


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