Browse Source

Merge branch 'master' into trac1024

Stephen Morris 13 years ago
parent
commit
72a0beb8df
74 changed files with 2876 additions and 489 deletions
  1. 10 0
      ChangeLog
  2. 13 0
      configure.ac
  3. 2 2
      doc/Doxyfile
  4. 1 2
      src/bin/bind10/bind10.py.in
  5. 4 1
      src/bin/bindctl/bindcmd.py
  6. 1 2
      src/bin/cmdctl/cmdctl.py.in
  7. 1 2
      src/bin/resolver/main.cc
  8. 137 112
      src/bin/resolver/resolver_messages.mes
  9. 10 0
      src/bin/stats/Makefile.am
  10. 37 30
      src/bin/stats/stats.py.in
  11. 45 54
      src/bin/stats/stats_httpd.py.in
  12. 92 0
      src/bin/stats/stats_httpd_messages.mes
  13. 75 0
      src/bin/stats/stats_messages.mes
  14. 8 0
      src/bin/stats/tests/Makefile.am
  15. 4 30
      src/bin/stats/tests/b10-stats-httpd_test.py
  16. 11 11
      src/bin/stats/tests/b10-stats_test.py
  17. 1 1
      src/bin/stats/tests/isc/Makefile.am
  18. 7 0
      src/bin/stats/tests/isc/log/Makefile.am
  19. 33 0
      src/bin/stats/tests/isc/log/__init__.py
  20. 2 3
      src/bin/xfrin/xfrin.py.in
  21. 1 1
      src/bin/xfrout/xfrout.py.in
  22. 2 2
      src/lib/Makefile.am
  23. 3 0
      src/lib/acl/acl.h
  24. 15 15
      src/lib/acl/loader.h
  25. 21 42
      src/lib/asiolink/tests/interval_timer_unittest.cc
  26. 2 2
      src/lib/config/ccsession.h
  27. 19 13
      src/lib/config/tests/ccsession_unittests.cc
  28. 2 1
      src/lib/datasrc/cache.cc
  29. 2 2
      src/lib/datasrc/data_source.cc
  30. 66 60
      src/lib/datasrc/datasrc_messages.mes
  31. 2 2
      src/lib/datasrc/memory_datasrc.cc
  32. 2 1
      src/lib/datasrc/sqlite3_datasrc.cc
  33. 1 1
      src/lib/datasrc/static_datasrc.cc
  34. 1 1
      src/lib/dns/tests/tsig_unittest.cc
  35. 65 27
      src/lib/log/compiler/message.cc
  36. 31 1
      src/lib/log/log_formatter.h
  37. 2 0
      src/lib/log/tests/Makefile.am
  38. 1 1
      src/lib/python/isc/Makefile.am
  39. 45 0
      src/lib/python/isc/acl/Makefile.am
  40. 11 0
      src/lib/python/isc/acl/__init__.py
  41. 80 0
      src/lib/python/isc/acl/acl.cc
  42. 29 0
      src/lib/python/isc/acl/acl.py
  43. 16 0
      src/lib/python/isc/acl/acl_inc.cc
  44. 135 0
      src/lib/python/isc/acl/dns.cc
  45. 52 0
      src/lib/python/isc/acl/dns.h
  46. 33 0
      src/lib/python/isc/acl/dns.py
  47. 33 0
      src/lib/python/isc/acl/dns_requestacl_inc.cc
  48. 184 0
      src/lib/python/isc/acl/dns_requestacl_python.cc
  49. 53 0
      src/lib/python/isc/acl/dns_requestacl_python.h
  50. 30 0
      src/lib/python/isc/acl/dns_requestcontext_inc.cc
  51. 319 0
      src/lib/python/isc/acl/dns_requestcontext_python.cc
  52. 54 0
      src/lib/python/isc/acl/dns_requestcontext_python.h
  53. 87 0
      src/lib/python/isc/acl/dns_requestloader_inc.cc
  54. 270 0
      src/lib/python/isc/acl/dns_requestloader_python.cc
  55. 46 0
      src/lib/python/isc/acl/dns_requestloader_python.h
  56. 17 0
      src/lib/python/isc/acl/dnsacl_inc.cc
  57. 30 0
      src/lib/python/isc/acl/tests/Makefile.am
  58. 29 0
      src/lib/python/isc/acl/tests/acl_test.py
  59. 280 0
      src/lib/python/isc/acl/tests/dns_test.py
  60. 12 4
      src/lib/python/isc/config/Makefile.am
  61. 8 10
      src/lib/python/isc/config/ccsession.py
  62. 33 0
      src/lib/python/isc/config/config_messages.mes
  63. 7 2
      src/lib/python/isc/config/tests/ccsession_test.py
  64. 11 0
      src/lib/python/isc/notify/Makefile.am
  65. 38 33
      src/lib/python/isc/notify/notify_out.py
  66. 83 0
      src/lib/python/isc/notify/notify_out_messages.mes
  67. 18 7
      src/lib/python/isc/notify/tests/notify_out_test.py
  68. 7 7
      src/lib/resolve/resolve_messages.mes
  69. 2 1
      src/lib/server_common/tests/keyring_test.cc
  70. 18 0
      src/lib/util/filename.cc
  71. 7 0
      src/lib/util/filename.h
  72. 28 1
      src/lib/util/python/pycppwrapper_util.h
  73. 2 2
      src/lib/util/python/wrapper_template.cc
  74. 37 0
      src/lib/util/tests/filename_unittest.cc

+ 10 - 0
ChangeLog

@@ -1,3 +1,13 @@
+270.	[func]		jinmei
+	Added python bindings for ACLs using the DNS request as the
+	context.  They are accessible via the isc.acl.dns module.
+	(Trac #983, git c24553e21fe01121a42e2136d0a1230d75812b27)
+
+269.	[bug]		y-aharen
+	Modified IntervalTimerTest not to rely on the accuracy of the timer.
+	This fix addresses occasional failure of build tests.
+	(Trac #1016, git 090c4c5abac33b2b28d7bdcf3039005a014f9c5b)
+
 268.	[func]		stephen
 	Add environment variable to allow redirection of logging output during
 	unit tests.

+ 13 - 0
configure.ac

@@ -139,6 +139,16 @@ else
 	AC_SUBST(pkgpyexecdir)
 fi
 
+# We need to store the default pyexecdir in a separate variable so that
+# we can specify in Makefile.am the install directory of various BIND 10
+# python scripts and loadable modules; in Makefile.am we cannot replace
+# $(pyexecdir) using itself, e.g, this doesn't work:
+# pyexecdir = $(pyexecdir)/isc/some_module
+# The separate variable makes this setup possible as follows:
+# pyexecdir = $(PYTHON_SITEPKG_DIR)/isc/some_module
+PYTHON_SITEPKG_DIR=${pyexecdir}
+AC_SUBST(PYTHON_SITEPKG_DIR)
+
 # Check for python development environments
 if test -x ${PYTHON}-config; then
 	PYTHON_INCLUDES=`${PYTHON}-config --includes`
@@ -793,6 +803,7 @@ AC_CONFIG_FILES([Makefile
                  src/bin/stats/tests/isc/cc/Makefile
                  src/bin/stats/tests/isc/config/Makefile
                  src/bin/stats/tests/isc/util/Makefile
+                 src/bin/stats/tests/isc/log/Makefile
                  src/bin/stats/tests/testdata/Makefile
                  src/bin/stats/tests/http/Makefile
                  src/bin/usermgr/Makefile
@@ -809,6 +820,8 @@ AC_CONFIG_FILES([Makefile
                  src/lib/cc/tests/Makefile
                  src/lib/python/Makefile
                  src/lib/python/isc/Makefile
+                 src/lib/python/isc/acl/Makefile
+                 src/lib/python/isc/acl/tests/Makefile
                  src/lib/python/isc/util/Makefile
                  src/lib/python/isc/util/tests/Makefile
                  src/lib/python/isc/datasrc/Makefile

+ 2 - 2
doc/Doxyfile

@@ -570,8 +570,8 @@ WARN_LOGFILE           =
 
 INPUT                  = ../src/lib/cc ../src/lib/config \
     ../src/lib/cryptolink ../src/lib/dns ../src/lib/datasrc \
-    ../src/bin/auth ../src/bin/resolver ../src/lib/bench \
-    ../src/lib/log ../src/lib/asiolink/ ../src/lib/nsas \
+    ../src/bin/auth ../src/bin/resolver ../src/lib/bench ../src/lib/log \
+    ../src/lib/log/compiler ../src/lib/asiolink/ ../src/lib/nsas \
     ../src/lib/testutils ../src/lib/cache ../src/lib/server_common/ \
     ../src/bin/sockcreator/ ../src/lib/util/ \
     ../src/lib/resolve ../src/lib/acl

+ 1 - 2
src/bin/bind10/bind10.py.in

@@ -462,8 +462,7 @@ class BoB:
         self.log_starting("ccsession")
         self.ccs = isc.config.ModuleCCSession(SPECFILE_LOCATION, 
                                       self.config_handler,
-                                      self.command_handler,
-                                      None, True)
+                                      self.command_handler)
         self.ccs.start()
         self.log_started()
 

+ 4 - 1
src/bin/bindctl/bindcmd.py

@@ -674,7 +674,10 @@ class BindCmdInterpreter(Cmd):
         elif cmd.command == "revert":
             self.config_data.clear_local_changes()
         elif cmd.command == "commit":
-            self.config_data.commit()
+            try:
+                self.config_data.commit()
+            except isc.config.ModuleCCSessionError as mcse:
+                print(str(mcse))
         elif cmd.command == "diff":
             print(self.config_data.get_local_changes());
         elif cmd.command == "go":

+ 1 - 2
src/bin/cmdctl/cmdctl.py.in

@@ -252,8 +252,7 @@ class CommandControl():
         self._cc = isc.cc.Session()
         self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
                                               self.config_handler,
-                                              self.command_handler,
-                                              None, True)
+                                              self.command_handler)
         self._module_name = self._module_cc.get_module_spec().get_module_name()
         self._cmdctl_config_data = self._module_cc.get_full_config()
         self._module_cc.start()

+ 1 - 2
src/bin/resolver/main.cc

@@ -208,8 +208,7 @@ main(int argc, char* argv[]) {
         cc_session = new Session(io_service.get_io_service());
         config_session = new ModuleCCSession(specfile, *cc_session,
                                              my_config_handler,
-                                             my_command_handler,
-                                             true, true);
+                                             my_command_handler);
         LOG_DEBUG(resolver_logger, RESOLVER_DBG_INIT, RESOLVER_CONFIG_CHANNEL);
 
         // FIXME: This does not belong here, but inside Boss

+ 137 - 112
src/bin/resolver/resolver_messages.mes

@@ -16,151 +16,174 @@
 # along with the resolver methods.
 
 % RESOLVER_AXFR_TCP AXFR request received over TCP
-A debug message, the resolver received a NOTIFY message over TCP.  The server
-cannot process it and will return an error message to the sender with the
-RCODE set to NOTIMP.
+This is a debug message output when the resolver received a request for
+an AXFR (full transfer of a zone) over TCP.  Only authoritative servers
+are able to handle AXFR requests, so the resolver will return an error
+message to the sender with the RCODE set to NOTIMP.
 
 % RESOLVER_AXFR_UDP AXFR request received over UDP
-A debug message, the resolver received a NOTIFY message over UDP.  The server
-cannot process it (and in any case, an AXFR request should be sent over TCP)
-and will return an error message to the sender with the RCODE set to FORMERR.
+This is a debug message output when the resolver received a request for
+an AXFR (full transfer of a zone) over UDP.  Only authoritative servers
+are able to handle AXFR requests (and in any case, an AXFR request should
+be sent over TCP), so the resolver will return an error message to the
+sender with the RCODE set to NOTIMP.
 
 % RESOLVER_CLIENT_TIME_SMALL client timeout of %1 is too small
-An error indicating that the configuration value specified for the query
-timeout is too small.
+During the update of the resolver's configuration parameters, the value
+of the client timeout was found to be too small.  The configuration
+update was abandoned and the parameters were not changed.
 
 % RESOLVER_CONFIG_CHANNEL configuration channel created
-A debug message, output when the resolver has successfully established a
-connection to the configuration channel.
+This is a debug message output when the resolver has successfully
+established a connection to the configuration channel.
 
 % RESOLVER_CONFIG_ERROR error in configuration: %1
-An error was detected in a configuration update received by the resolver. This
-may be in the format of the configuration message (in which case this is a
-programming error) or it may be in the data supplied (in which case it is
-a user error).  The reason for the error, given as a parameter in the message,
-will give more details.
+An error was detected in a configuration update received by the
+resolver. This may be in the format of the configuration message (in
+which case this is a programming error) or it may be in the data supplied
+(in which case it is a user error).  The reason for the error, included
+in the message, will give more details.  The configuration update is
+not applied and the resolver parameters were not changed.
 
 % RESOLVER_CONFIG_LOADED configuration loaded
-A debug message, output when the resolver configuration has been successfully
-loaded.
+This is a debug message output when the resolver configuration has been
+successfully loaded.
 
 % RESOLVER_CONFIG_UPDATED configuration updated: %1
-A debug message, the configuration has been updated with the specified
-information.
+This is a debug message output when the resolver configuration is being
+updated with the specified information.
 
 % RESOLVER_CREATED main resolver object created
-A debug message, output when the Resolver() object has been created.
+This is a debug message indicating that the main resolver object has
+been created.
 
 % RESOLVER_DNS_MESSAGE_RECEIVED DNS message received: %1
-A debug message, this always precedes some other logging message and is the
-formatted contents of the DNS packet that the other message refers to.
+This is a debug message from the resolver listing the contents of a
+received DNS message.
 
 % RESOLVER_DNS_MESSAGE_SENT DNS message of %1 bytes sent: %2
-A debug message, this contains details of the response sent back to the querying
-system.
+This is a debug message containing details of the response returned by
+the resolver to the querying system.
 
 % RESOLVER_FAILED resolver failed, reason: %1
-This is an error message output when an unhandled exception is caught by the
-resolver.  All it can do is to shut down.
+This is an error message output when an unhandled exception is caught
+by the resolver.  After this, the resolver will shut itself down.
+Please submit a bug report.
 
 % RESOLVER_FORWARD_ADDRESS setting forward address %1(%2)
-This message may appear multiple times during startup, and it lists the
-forward addresses used by the resolver when running in forwarding mode.
+If the resolver is running in forward mode, this message will appear
+during startup to list the forward address.  If multiple addresses are
+specified, it will appear once for each address.
 
 % RESOLVER_FORWARD_QUERY processing forward query
-The received query has passed all checks and is being forwarded to upstream
+This is a debug message indicating that a query received by the resolver
+has passed a set of checks (message is well-formed, it is allowed by the
+ACL, it is a supported opcode etc.) and is being forwarded to upstream
 servers.
 
 % RESOLVER_HEADER_ERROR message received, exception when processing header: %1
-A debug message noting that an exception occurred during the processing of
-a received packet.  The packet has been dropped.
+This is a debug message from the resolver noting that an exception
+occurred during the processing of a received packet.  The packet has
+been dropped.
 
 % RESOLVER_IXFR IXFR request received
-The resolver received a NOTIFY message over TCP.  The server cannot process it
-and will return an error message to the sender with the RCODE set to NOTIMP.
+This is a debug message indicating that the resolver received a request
+for an IXFR (incremental transfer of a zone).  Only authoritative servers
+are able to handle IXFR requests, so the resolver will return an error
+message to the sender with the RCODE set to NOTIMP.
 
 % RESOLVER_LOOKUP_TIME_SMALL lookup timeout of %1 is too small
-An error indicating that the configuration value specified for the lookup
-timeout is too small.
+During the update of the resolver's configuration parameters, the value
+of the lookup timeout was found to be too small.  The configuration
+update will not be applied.
 
 % RESOLVER_MESSAGE_ERROR error parsing received message: %1 - returning %2
-A debug message noting that the resolver received a message and the
-parsing of the body of the message failed due to some error (although
-the parsing of the header succeeded).  The message parameters give a
-textual description of the problem and the RCODE returned.
+This is a debug message noting that parsing of the body of a received
+message by the resolver failed due to some error (although the parsing of
+the header succeeded).  The message parameters give a textual description
+of the problem and the RCODE returned.
 
 % RESOLVER_NEGATIVE_RETRIES negative number of retries (%1) specified in the configuration
-An error message indicating that the resolver configuration has specified a
-negative retry count.  Only zero or positive values are valid.
+This error is issued when a resolver configuration update has specified
+a negative retry count: only zero or positive values are valid.  The
+configuration update was abandoned and the parameters were not changed.
 
 % RESOLVER_NON_IN_PACKET non-IN class request received, returning REFUSED message
-A debug message, the resolver has received a DNS packet that was not IN class.
-The resolver cannot handle such packets, so is returning a REFUSED response to
-the sender.
+This debug message is issued when resolver has received a DNS packet that
+was not IN (Internet) class.  The resolver cannot handle such packets,
+so is returning a REFUSED response to the sender.
 
 % RESOLVER_NORMAL_QUERY processing normal query
-The received query has passed all checks and is being processed by the resolver.
+This is a debug message indicating that the query received by the resolver
+has passed a set of checks (message is well-formed, it is allowed by the
+ACL, it is a supported opcode etc.) and is being processed the resolver.
 
 % RESOLVER_NOTIFY_RECEIVED NOTIFY arrived but server is not authoritative
-The resolver received a NOTIFY message.  As the server is not authoritative it
-cannot process it, so it returns an error message to the sender with the RCODE
-set to NOTAUTH.
+The resolver has received a NOTIFY message.  As the server is not
+authoritative it cannot process it, so it returns an error message to
+the sender with the RCODE set to NOTAUTH.
 
 % RESOLVER_NOT_ONE_QUESTION query contained %1 questions, exactly one question was expected
-A debug message, the resolver received a query that contained the number of
-entires in the question section detailed in the message.  This is a malformed
-message, as a DNS query must contain only one question.  The resolver will
-return a message to the sender with the RCODE set to FORMERR.
+This debug message indicates that the resolver received a query that
+contained the number of entries in the question section detailed in
+the message.  This is a malformed message, as a DNS query must contain
+only one question.  The resolver will return a message to the sender
+with the RCODE set to FORMERR.
 
 % RESOLVER_NO_ROOT_ADDRESS no root addresses available
-A warning message during startup, indicates that no root addresses have been
-set.  This may be because the resolver will get them from a priming query.
+A warning message issued during resolver startup, this indicates that
+no root addresses have been set.  This may be because the resolver will
+get them from a priming query.
 
 % RESOLVER_PARSE_ERROR error parsing received message: %1 - returning %2
-A debug message noting that the resolver received a message and the parsing
-of the body of the message failed due to some non-protocol related reason
-(although the parsing of the header succeeded).  The message parameters give
-a textual description of the problem and the RCODE returned.
+This is a debug message noting that the resolver received a message and
+the parsing of the body of the message failed due to some non-protocol
+related reason (although the parsing of the header succeeded).
+The message parameters give a textual description of the problem and
+the RCODE returned.
 
 % RESOLVER_PRINT_COMMAND print message command, arguments are: %1
-This message is logged when a "print_message" command is received over the
-command channel.
+This debug message is logged when a "print_message" command is received
+by the resolver over the command channel.
 
 % RESOLVER_PROTOCOL_ERROR protocol error parsing received message: %1 - returning %2
-A debug message noting that the resolver received a message and 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.
+This is a debug message noting that the resolver received a message and
+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_SETUP query setup
-A debug message noting that the resolver is creating a RecursiveQuery object.
+This is a debug message noting that the resolver is creating a
+RecursiveQuery object.
 
 % RESOLVER_QUERY_SHUTDOWN query shutdown
-A debug message noting that the resolver is destroying a RecursiveQuery object.
+This is a debug message noting that the resolver is destroying a
+RecursiveQuery object.
 
 % RESOLVER_QUERY_TIME_SMALL query timeout of %1 is too small
-An error indicating that the configuration value specified for the query
-timeout is too small.
+During the update of the resolver's configuration parameters, the value
+of the query timeout was found to be too small.  The configuration
+parameters were not changed.
 
 % RESOLVER_RECEIVED_MESSAGE resolver has received a DNS message
-A debug message indicating that the resolver has received a message.  Depending
-on the debug settings, subsequent log output will indicate the nature of the
-message.
+This is a debug message indicating that the resolver has received a
+DNS message.  Depending on the debug settings, subsequent log output
+will indicate the nature of the message.
 
 % RESOLVER_RECURSIVE running in recursive mode
-This is an informational message that appears at startup noting that the
-resolver is running in recursive mode.
+This is an informational message that appears at startup noting that
+the resolver is running in recursive mode.
 
 % RESOLVER_SERVICE_CREATED service object created
-A debug message, output when the main service object (which handles the
-received queries) is created.
+This debug message is output when resolver creates the main service object
+(which handles the received queries).
 
 % RESOLVER_SET_PARAMS query timeout: %1, client timeout: %2, lookup timeout: %3, retry count: %4
-A debug message, lists the parameters being set for the resolver.  These are:
+This debug message lists the parameters being set for the resolver.  These are:
 query timeout: the timeout (in ms) used for queries originated by the resolver
-to upstream servers.  Client timeout: the interval to resolver a query by
+to upstream servers.  Client timeout: the interval to resolve a query by
 a client: after this time, the resolver sends back a SERVFAIL to the client
-whilst continuing to resolver the query. Lookup timeout: the time at which the
+whilst continuing to resolve the query. Lookup timeout: the time at which the
 resolver gives up trying to resolve a query.  Retry count: the number of times
 the resolver will retry a query to an upstream server if it gets a timeout.
 
@@ -169,17 +192,18 @@ resolution of the client query might require a large number of queries to
 upstream nameservers.  Even if none of these queries timeout, the total time
 taken to perform all the queries may exceed the client timeout.  When this
 happens, a SERVFAIL is returned to the client, but the resolver continues
-with the resolution process. Data received is added to the cache.  However,
+with the resolution process; data received is added to the cache.  However,
 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_ROOT_ADDRESS setting root address %1(%2)
-This message may appear multiple times during startup; it lists the root
-addresses used by the resolver.
+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,
+once for each root server address.
 
 % RESOLVER_SHUTDOWN resolver shutdown complete
-This information message is output when the resolver has shut down.
+This informational message is output when the resolver has shut down.
 
 % RESOLVER_STARTED resolver started
 This informational message is output by the resolver when all initialization
@@ -189,35 +213,36 @@ has been completed and it is entering its main loop.
 An informational message, this is output when the resolver starts up.
 
 % RESOLVER_UNEXPECTED_RESPONSE received unexpected response, ignoring
-A debug message noting that the server has received a response instead of a
-query and is ignoring it.
+This is a debug message noting that the resolver received a DNS response
+packet on the port on which is it listening for queries.  The packet
+has been ignored.
 
 % RESOLVER_UNSUPPORTED_OPCODE opcode %1 not supported by the resolver
-A debug message, 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
-A debug message that appears when a new query ACL is configured for the
-resolver.
-
-% RESOLVER_QUERY_ACCEPTED   query accepted: '%1/%2/%3' from %4
-A debug message that indicates 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
-An informational message that indicates an incoming query is rejected
-in terms 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
-An informational message that indicates an incoming query is dropped
-in terms 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>.
+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>.

+ 10 - 0
src/bin/stats/Makefile.am

@@ -7,14 +7,18 @@ pkglibexec_SCRIPTS = b10-stats b10-stats-httpd
 b10_statsdir = $(pkgdatadir)
 b10_stats_DATA = stats.spec stats-httpd.spec stats-schema.spec
 b10_stats_DATA += stats-httpd-xml.tpl stats-httpd-xsd.tpl stats-httpd-xsl.tpl
+pyexec_DATA = stats_messages.py stats_httpd_messages.py
 
 CLEANFILES = b10-stats stats.pyc
 CLEANFILES += b10-stats-httpd stats_httpd.pyc
+CLEANFILES += stats_messages.py stats_messages.pyc
+CLEANFILES += stats_httpd_messages.py stats_httpd_messages.pyc
 
 man_MANS = b10-stats.8 b10-stats-httpd.8
 EXTRA_DIST = $(man_MANS) b10-stats.xml b10-stats-httpd.xml
 EXTRA_DIST += stats.spec stats-httpd.spec stats-schema.spec
 EXTRA_DIST += stats-httpd-xml.tpl stats-httpd-xsd.tpl stats-httpd-xsl.tpl
+EXTRA_DIST += stats_messages.mes stats_httpd_messages.mes
 
 if ENABLE_MAN
 
@@ -26,6 +30,12 @@ b10-stats-httpd.8: b10-stats-httpd.xml
 
 endif
 
+stats_messages.py: stats_messages.mes
+	$(top_builddir)/src/lib/log/compiler/message -p $(top_srcdir)/src/bin/stats/stats_messages.mes
+
+stats_httpd_messages.py: stats_httpd_messages.mes
+	$(top_builddir)/src/lib/log/compiler/message -p $(top_srcdir)/src/bin/stats/stats_httpd_messages.mes
+
 # this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix
 b10-stats: stats.py
 	$(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|"  stats.py >$@

+ 37 - 30
src/bin/stats/stats.py.in

@@ -25,6 +25,16 @@ from collections import defaultdict
 from isc.config.ccsession import ModuleCCSession, create_answer
 from isc.cc import Session, SessionError
 
+import isc.log
+from stats_messages import *
+
+isc.log.init("b10-stats")
+logger = isc.log.Logger("stats")
+
+# Some constants for debug levels, these should be removed when we
+# have #1074
+DBG_STATS_MESSAGING = 30
+
 # for setproctitle
 import isc.util.process
 isc.util.process.rename()
@@ -143,9 +153,8 @@ class SessionSubject(Subject, metaclass=Singleton):
     """
     A concrete subject class which creates CC session object
     """
-    def __init__(self, session=None, verbose=False):
+    def __init__(self, session=None):
         Subject.__init__(self)
-        self.verbose = verbose
         self.session=session
         self.running = False
 
@@ -165,9 +174,8 @@ class CCSessionListener(Listener):
     A concrete listener class which creates SessionSubject object and
     ModuleCCSession object
     """
-    def __init__(self, subject, verbose=False):
+    def __init__(self, subject):
         Listener.__init__(self, subject)
-        self.verbose = verbose
         self.session = subject.session
         self.boot_time = get_datetime()
 
@@ -203,8 +211,7 @@ class CCSessionListener(Listener):
                 kwargs = self.initialize_data(cmd["command_args"])
                 self.add_event(Callback(name=name, callback=callback, args=(), kwargs=kwargs))
             except AttributeError as ae:
-                sys.stderr.write("[b10-stats] Caught undefined command while parsing spec file: "
-                                 +str(cmd["command_name"])+"\n")
+                logger.error(STATS_UNKNOWN_COMMAND_IN_SPEC, cmd["command_name"])
 
     def start(self):
         """
@@ -217,8 +224,7 @@ class CCSessionListener(Listener):
         self.stats_data['stats.lname'] = self.session.lname
         self.cc_session.start()
         # request Bob to send statistics data
-        if self.verbose:
-            sys.stdout.write("[b10-stats] request Bob to send statistics data\n")
+        logger.debug(DBG_STATS_MESSAGING, STATS_SEND_REQUEST_BOSS)
         cmd = isc.config.ccsession.create_command("sendstats", None)
         seq = self.session.group_sendmsg(cmd, 'Boss')
         self.session.group_recvmsg(True, seq)
@@ -239,8 +245,8 @@ class CCSessionListener(Listener):
         """
         handle a configure from the cc channel
         """
-        if self.verbose:
-            sys.stdout.write("[b10-stats] newconfig received: "+str(new_config)+"\n")
+        logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_NEW_CONFIG,
+                     new_config)
 
         # do nothing currently
         return create_answer(0)
@@ -262,8 +268,7 @@ class CCSessionListener(Listener):
         """
         handle shutdown command
         """
-        if self.verbose:
-            sys.stdout.write("[b10-stats] 'shutdown' command received\n")
+        logger.info(STATS_RECEIVED_SHUTDOWN_COMMAND)
         self.subject.running = False
         return create_answer(0)
 
@@ -283,13 +288,14 @@ class CCSessionListener(Listener):
         """
         handle remove command
         """
-        if self.verbose:
-            sys.stdout.write("[b10-stats] 'remove' command received, args: "+str(args)+"\n")
 
         # 'args' must be dictionary type
         if args and args['stats_item_name'] in self.stats_data:
             stats_item_name = args['stats_item_name']
 
+        logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_REMOVE_COMMAND,
+                     stats_item_name)
+
         # just remove one item
         self.stats_data.pop(stats_item_name)
 
@@ -299,8 +305,6 @@ class CCSessionListener(Listener):
         """
         handle show command
         """
-        if self.verbose:
-            sys.stdout.write("[b10-stats] 'show' command received, args: "+str(args)+"\n")
 
         # always overwrite 'report_time' and 'stats.timestamp'
         # if "show" command invoked
@@ -310,16 +314,21 @@ class CCSessionListener(Listener):
         # if with args
         if args and args['stats_item_name'] in self.stats_data:
             stats_item_name = args['stats_item_name']
+            logger.debug(DBG_STATS_MESSAGING,
+                         STATS_RECEIVED_SHOW_NAME_COMMAND,
+                         stats_item_name)
             return create_answer(0, {stats_item_name: self.stats_data[stats_item_name]})
 
+        logger.debug(DBG_STATS_MESSAGING,
+                     STATS_RECEIVED_SHOW_ALL_COMMAND)
         return create_answer(0, self.stats_data)
 
     def command_reset(self, args):
         """
         handle reset command
         """
-        if self.verbose:
-            sys.stdout.write("[b10-stats] 'reset' command received\n")
+        logger.debug(DBG_STATS_MESSAGING,
+                     STATS_RECEIVED_RESET_COMMAND)
 
         # re-initialize internal variables
         self.stats_data = self.initialize_data(self.stats_spec)
@@ -336,8 +345,7 @@ class CCSessionListener(Listener):
         """
         handle status command
         """
-        if self.verbose:
-            sys.stdout.write("[b10-stats] 'status' command received\n")
+        logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_STATUS_COMMAND)
         # just return "I'm alive."
         return create_answer(0, "I'm alive.")
 
@@ -345,9 +353,7 @@ class CCSessionListener(Listener):
         """
         handle an unknown command
         """
-        if self.verbose:
-            sys.stdout.write("[b10-stats] Unknown command received: '"
-                             + str(command) + "'\n")
+        logger.error(STATS_RECEIVED_UNKNOWN_COMMAND, command)
         return create_answer(1, "Unknown command: '"+str(command)+"'")
 
 
@@ -394,20 +400,21 @@ def main(session=None):
         parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
                       help="display more about what is going on")
         (options, args) = parser.parse_args()
-        subject = SessionSubject(session=session, verbose=options.verbose)
-        listener = CCSessionListener(subject, verbose=options.verbose)
+        if options.verbose:
+            isc.log.init("b10-stats", "DEBUG", 99)
+        subject = SessionSubject(session=session)
+        listener = CCSessionListener(subject)
         subject.start()
         while subject.running:
             subject.check()
         subject.stop()
 
-    except OptionValueError:
-        sys.stderr.write("[b10-stats] Error parsing options\n")
+    except OptionValueError as ove:
+        logger.fatal(STATS_BAD_OPTION_VALUE, ove)
     except SessionError as se:
-        sys.stderr.write("[b10-stats] Error creating Stats module, "
-              + "is the command channel daemon running?\n")
+        logger.fatal(STATS_CC_SESSION_ERROR, se)
     except KeyboardInterrupt as kie:
-        sys.stderr.write("[b10-stats] Interrupted, exiting\n")
+        logger.info(STATS_STOPPED_BY_KEYBOARD)
 
 if __name__ == "__main__":
     main()

+ 45 - 54
src/bin/stats/stats_httpd.py.in

@@ -34,6 +34,17 @@ import isc.cc
 import isc.config
 import isc.util.process
 
+import isc.log
+from stats_httpd_messages import *
+
+isc.log.init("b10-stats-httpd")
+logger = isc.log.Logger("stats-httpd")
+
+# Some constants for debug levels, these should be removed when we
+# have #1074
+DBG_STATHTTPD_INIT = 10
+DBG_STATHTTPD_MESSAGING = 30
+
 # 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
@@ -98,9 +109,7 @@ class HttpHandler(http.server.BaseHTTPRequestHandler):
                     return None
         except StatsHttpdError as err:
             self.send_error(500)
-            if self.server.verbose:
-                self.server.log_writer(
-                    "[b10-stats-httpd] %s\n" % err)
+            logger.error(STATHTTPD_SERVER_ERROR, err)
             return None
         else:
             self.send_response(200)
@@ -109,15 +118,6 @@ class HttpHandler(http.server.BaseHTTPRequestHandler):
             self.end_headers()
             return body
 
-    def log_message(self, format, *args):
-        """Change the default log format"""
-        if self.server.verbose:
-            self.server.log_writer(
-                "[b10-stats-httpd] %s - - [%s] %s\n" %
-                (self.address_string(),
-                 self.log_date_time_string(),
-                 format%args))
-
 class HttpServerError(Exception):
     """Exception class for HttpServer class. It is intended to be
     passed from the HttpServer object to the StatsHttpd object."""
@@ -134,13 +134,12 @@ class HttpServer(http.server.HTTPServer):
     sys.stderr.write. They are intended to be referred by HttpHandler
     object."""
     def __init__(self, server_address, handler,
-                 xml_handler, xsd_handler, xsl_handler, log_writer, verbose=False):
+                 xml_handler, xsd_handler, xsl_handler, log_writer):
         self.server_address = server_address
         self.xml_handler = xml_handler
         self.xsd_handler = xsd_handler
         self.xsl_handler = xsl_handler
         self.log_writer = log_writer
-        self.verbose = verbose
         http.server.HTTPServer.__init__(self, server_address, handler)
 
 class StatsHttpdError(Exception):
@@ -154,8 +153,7 @@ class StatsHttpd:
     statistics module. It handles HTTP requests, and command channel
     and config channel CC session. It uses select.select function
     while waiting for clients requests."""
-    def __init__(self, verbose=False):
-        self.verbose = verbose
+    def __init__(self):
         self.running = False
         self.poll_intval = 0.5
         self.write_log = sys.stderr.write
@@ -169,8 +167,7 @@ class StatsHttpd:
     def open_mccs(self):
         """Opens a ModuleCCSession object"""
         # create ModuleCCSession
-        if self.verbose:
-            self.write_log("[b10-stats-httpd] Starting CC Session\n")
+        logger.debug(DBG_STATHTTPD_INIT, STATHTTPD_STARTING_CC_SESSION)
         self.mccs = isc.config.ModuleCCSession(
             SPECFILE_LOCATION, self.config_handler, self.command_handler)
         self.cc_session = self.mccs._session
@@ -183,8 +180,8 @@ class StatsHttpd:
         """Closes a ModuleCCSession object"""
         if self.mccs is None:
             return
-        if self.verbose:
-            self.write_log("[b10-stats-httpd] Closing CC Session\n")
+
+        logger.debug(DBG_STATHTTPD_INIT, STATHTTPD_CLOSING_CC_SESSION)
         self.mccs.close()
         self.mccs = None
 
@@ -221,7 +218,7 @@ class StatsHttpd:
             httpd = HttpServer(
                 server_address, HttpHandler,
                 self.xml_handler, self.xsd_handler, self.xsl_handler,
-                self.write_log, self.verbose)
+                self.write_log)
         except (socket.gaierror, socket.error,
                 OverflowError, TypeError) as err:
             # try IPv4 next
@@ -233,10 +230,8 @@ class StatsHttpd:
                     (server_address[0], server_address[1],
                      err.__class__.__name__, err))
         else:
-            if self.verbose:
-                self.write_log(
-                    "[b10-stats-httpd] Started on address %s, port %s\n" %
-                    server_address)
+            logger.info(STATHTTPD_STARTED, server_address[0],
+                        server_address[1])
         return httpd
 
     def close_httpd(self):
@@ -244,11 +239,8 @@ class StatsHttpd:
         if len(self.httpd) == 0:
             return
         for ht in self.httpd:
-            if self.verbose:
-                self.write_log(
-                    "[b10-stats-httpd] Closing address %s, port %s\n" %
-                    (ht.server_address[0], ht.server_address[1])
-                    )
+            logger.info(STATHTTPD_CLOSING, ht.server_address[0],
+                        ht.server_address[1])
             ht.server_close()
         self.httpd = []
 
@@ -285,8 +277,7 @@ class StatsHttpd:
     def stop(self):
         """Stops the running StatsHttpd objects. Closes CC session and
         HTTP handling sockets"""
-        if self.verbose:
-            self.write_log("[b10-stats-httpd] Shutting down\n")
+        logger.info(STATHTTPD_SHUTDOWN)
         self.close_httpd()
         self.close_mccs()
 
@@ -303,13 +294,11 @@ class StatsHttpd:
     def config_handler(self, new_config):
         """Config handler for the ModuleCCSession object. It resets
         addresses and ports to listen HTTP requests on."""
-        if self.verbose:
-            self.write_log("[b10-stats-httpd] Loading config : %s\n" % str(new_config))
+        logger.debug(DBG_STATHTTPD_MESSAGING, STATHTTPD_HANDLE_CONFIG,
+                   new_config)
         for key in new_config.keys():
-            if key not in DEFAULT_CONFIG:
-                if self.verbose:
-                    self.write_log(
-                        "[b10-stats-httpd] Unknown known config: %s" % key)
+            if key not in DEFAULT_CONFIG and key != "version":
+                logger.error(STATHTTPD_UNKNOWN_CONFIG_ITEM, key)
                 return isc.config.ccsession.create_answer(
                     1, "Unknown known config: %s" % key)
         # backup old config
@@ -319,9 +308,7 @@ class StatsHttpd:
         try:
             self.open_httpd()
         except HttpServerError as err:
-            if self.verbose:
-                self.write_log("[b10-stats-httpd] %s\n" % err)
-                self.write_log("[b10-stats-httpd] Restoring old config\n")
+            logger.error(STATHTTPD_SERVER_ERROR, err)
             # restore old config
             self.config_handler(old_config)
             return isc.config.ccsession.create_answer(
@@ -333,19 +320,19 @@ class StatsHttpd:
         """Command handler for the ModuleCCSesson object. It handles
         "status" and "shutdown" commands."""
         if command == "status":
-            if self.verbose:
-                self.write_log("[b10-stats-httpd] Received 'status' command\n")
+            logger.debug(DBG_STATHTTPD_MESSAGING,
+                         STATHTTPD_RECEIVED_STATUS_COMMAND)
             return isc.config.ccsession.create_answer(
                 0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")")
         elif command == "shutdown":
-            if self.verbose:
-                self.write_log("[b10-stats-httpd] Received 'shutdown' command\n")
+            logger.debug(DBG_STATHTTPD_MESSAGING,
+                         STATHTTPD_RECEIVED_SHUTDOWN_COMMAND)
             self.running = False
             return isc.config.ccsession.create_answer(
                 0, "Stats Httpd is shutting down.")
         else:
-            if self.verbose:
-                self.write_log("[b10-stats-httpd] Received unknown command\n")
+            logger.debug(DBG_STATHTTPD_MESSAGING,
+                         STATHTTPD_RECEIVED_UNKNOWN_COMMAND, command)
             return isc.config.ccsession.create_answer(
                 1, "Unknown command: " + str(command))
 
@@ -479,14 +466,18 @@ if __name__ == "__main__":
             "-v", "--verbose", dest="verbose", action="store_true",
             help="display more about what is going on")
         (options, args) = parser.parse_args()
-        stats_httpd = StatsHttpd(verbose=options.verbose)
+        if options.verbose:
+            isc.log.init("b10-stats-httpd", "DEBUG", 99)
+        stats_httpd = StatsHttpd()
         stats_httpd.start()
-    except OptionValueError:
-        sys.exit("[b10-stats-httpd] Error parsing options")
+    except OptionValueError as ove:
+        logger.fatal(STATHTTPD_BAD_OPTION_VALUE, ove)
+        sys.exit(1)
     except isc.cc.session.SessionError as se:
-        sys.exit("[b10-stats-httpd] Error creating module, "
-                 + "is the command channel daemon running?")
+        logger.fatal(STATHTTPD_CC_SESSION_ERROR, se)
+        sys.exit(1)
     except HttpServerError as hse:
-        sys.exit("[b10-stats-httpd] %s" % hse)
+        logger.fatal(STATHTTPD_START_SERVER_ERROR, hse)
+        sys.exit(1)
     except KeyboardInterrupt as kie:
-        sys.exit("[b10-stats-httpd] Interrupted, exiting")
+        logger.info(STATHTTPD_STOPPED_BY_KEYBOARD)

+ 92 - 0
src/bin/stats/stats_httpd_messages.mes

@@ -0,0 +1,92 @@
+# Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+# No namespace declaration - these constants go in the global namespace
+# of the stats_httpd_messages python module.
+
+% STATHTTPD_BAD_OPTION_VALUE bad command line argument: %1
+The stats-httpd module was called with a bad command-line argument
+and will not start.
+
+% STATHTTPD_CC_SESSION_ERROR error connecting to message bus: %1
+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_HANDLE_CONFIG reading configuration: %1
+The stats-httpd daemon has received new configuration data and will now
+process it. The (changed) data is printed.
+
+% STATHTTPD_RECEIVED_SHUTDOWN_COMMAND shutdown command received
+A shutdown command was sent to the stats-httpd module, and it will
+now shut down.
+
+% STATHTTPD_RECEIVED_STATUS_COMMAND received command to return status
+A status command was sent to the stats-httpd module, and it will
+respond with 'Stats Httpd is up.' and its PID.
+
+% STATHTTPD_RECEIVED_UNKNOWN_COMMAND received unknown command: %1
+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. A 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
+is a port binding problem or a bad configuration value. The specific
+error is printed in the message. The new configuration is ignored,
+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.
+
+% STATHTTPD_STARTING_CC_SESSION starting cc session
+Debug message indicating that the stats-httpd module is connecting to
+the command and control bus.
+
+% 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.
+
+% STATHTTPD_UNKNOWN_CONFIG_ITEM unknown configuration item: %1
+The stats-httpd daemon received a configuration update from the
+configuration manager. However, one of the items in the
+configuration is unknown. The new configuration is ignored, and an
+error is sent back. As possible cause is that there was an upgrade
+problem, and the stats-httpd version is out of sync with the rest of
+the system.

+ 75 - 0
src/bin/stats/stats_messages.mes

@@ -0,0 +1,75 @@
+# Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+# No namespace declaration - these constants go in the global namespace
+# of the stats_messages python module.
+
+% STATS_BAD_OPTION_VALUE bad command line argument: %1
+The stats module was called with a bad command-line argument and will
+not start.
+
+% STATS_CC_SESSION_ERROR error connecting to message bus: %1
+The stats 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 module will now shut down.
+
+% STATS_RECEIVED_NEW_CONFIG received new configuration: %1
+This debug message is printed when the stats module has received a
+configuration update from the configuration manager.
+
+% STATS_RECEIVED_REMOVE_COMMAND received command to remove %1
+A remove command for the given name was sent to the stats module, and
+the given statistics value will now be removed. It will not appear in
+statistics reports until it appears in a statistics update from a
+module again.
+
+% STATS_RECEIVED_RESET_COMMAND received command to reset all statistics
+The stats module received a command to clear all collected statistics.
+The data is cleared until it receives an update from the modules again.
+
+% 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.
+
+% STATS_RECEIVED_SHOW_NAME_COMMAND received command to show statistics for %1
+The stats module received a command to show the statistics that it has
+collected for the given item.
+
+% STATS_RECEIVED_SHUTDOWN_COMMAND shutdown command received
+A shutdown command was sent to the stats module and it will now shut down.
+
+% STATS_RECEIVED_STATUS_COMMAND received command to return status
+A status command was sent to the stats module. It will return a
+response indicating that it is running normally.
+
+% STATS_RECEIVED_UNKNOWN_COMMAND received unknown command: %1
+An unknown command has been sent to the stats module. The stats module
+will respond with an error and the command will be ignored.
+
+% STATS_SEND_REQUEST_BOSS requesting boss to send statistics
+This debug message is printed when a request is sent to the boss module
+to send its data to the stats module.
+
+% 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.
+
+% STATS_UNKNOWN_COMMAND_IN_SPEC unknown command in specification file: %1
+The specification file for the stats module contains a command that
+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.
+
+

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

@@ -4,6 +4,13 @@ PYTESTS = b10-stats_test.py b10-stats-httpd_test.py
 EXTRA_DIST = $(PYTESTS) fake_time.py fake_socket.py fake_select.py
 CLEANFILES = fake_time.pyc fake_socket.pyc fake_select.pyc
 
+# If necessary (rare cases), explicitly specify paths to dynamic libraries
+# required by loadable python modules.
+LIBRARY_PATH_PLACEHOLDER =
+if SET_ENV_LIBRARY_PATH
+LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(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:$$$(ENV_LIBRARY_PATH)
+endif
+
 # test using command-line arguments, so use check-local target instead of TESTS
 check-local:
 if ENABLE_PYTHON_COVERAGE
@@ -13,6 +20,7 @@ if ENABLE_PYTHON_COVERAGE
 endif
 	for pytest in $(PYTESTS) ; do \
 	echo Running test: $$pytest ; \
+	$(LIBRARY_PATH_PLACEHOLDER) \
 	env PYTHONPATH=$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python:$(abs_top_builddir)/src/bin/stats:$(abs_top_builddir)/src/bin/stats/tests \
 	B10_FROM_SOURCE=$(abs_top_srcdir) \
 	$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \

+ 4 - 30
src/bin/stats/tests/b10-stats-httpd_test.py

@@ -57,13 +57,9 @@ class TestHttpHandler(unittest.TestCase):
     """Tests for HttpHandler class"""
 
     def setUp(self):
-        self.verbose = True
-        self.stats_httpd = stats_httpd.StatsHttpd(self.verbose)
+        self.stats_httpd = stats_httpd.StatsHttpd()
         self.assertTrue(type(self.stats_httpd.httpd) is list)
         self.httpd = self.stats_httpd.httpd
-        for ht in self.httpd:
-            self.assertTrue(ht.verbose)
-        self.stats_httpd.cc_session.verbose = False
 
     def test_do_GET(self):
         for ht in self.httpd:
@@ -155,21 +151,6 @@ class TestHttpHandler(unittest.TestCase):
         handler.do_HEAD()
         self.assertEqual(handler.response.code, 404)
 
-    def test_log_message(self):
-        for ht in self.httpd:
-            self._test_log_message(ht._handler)
-
-    def _test_log_message(self, handler):
-        # switch write_log function
-        handler.server.log_writer = handler.response._write_log
-        log_message = 'ABCDEFG'
-        handler.log_message("%s", log_message)
-        self.assertEqual(handler.response.log, 
-                         "[b10-stats-httpd] %s - - [%s] %s\n" %
-                         (handler.address_string(),
-                          handler.log_date_time_string(),
-                          log_message))
-
 class TestHttpServerError(unittest.TestCase):
     """Tests for HttpServerError exception"""
 
@@ -183,12 +164,9 @@ class TestHttpServer(unittest.TestCase):
     """Tests for HttpServer class"""
 
     def test_httpserver(self):
-        self.verbose = True
-        self.stats_httpd = stats_httpd.StatsHttpd(self.verbose)
-        self.stats_httpd.cc_session.verbose = False
+        self.stats_httpd = stats_httpd.StatsHttpd()
         for ht in self.stats_httpd.httpd:
             self.assertTrue(ht.server_address in self.stats_httpd.http_addrs)
-            self.assertEqual(ht.verbose, self.verbose)
             self.assertEqual(ht.xml_handler, self.stats_httpd.xml_handler)
             self.assertEqual(ht.xsd_handler, self.stats_httpd.xsd_handler)
             self.assertEqual(ht.xsl_handler, self.stats_httpd.xsl_handler)
@@ -209,17 +187,14 @@ class TestStatsHttpd(unittest.TestCase):
     """Tests for StatsHttpd class"""
 
     def setUp(self):
-        self.verbose = True
         fake_socket._CLOSED = False
         fake_socket.has_ipv6 = True
-        self.stats_httpd = stats_httpd.StatsHttpd(self.verbose)
-        self.stats_httpd.cc_session.verbose = False
+        self.stats_httpd = stats_httpd.StatsHttpd()
 
     def tearDown(self):
         self.stats_httpd.stop()
 
     def test_init(self):
-        self.assertTrue(self.stats_httpd.verbose)
         self.assertFalse(self.stats_httpd.mccs.get_socket()._closed)
         self.assertEqual(self.stats_httpd.mccs.get_socket().fileno(),
                          id(self.stats_httpd.mccs.get_socket()))
@@ -317,8 +292,7 @@ class TestStatsHttpd(unittest.TestCase):
         self.stats_httpd.cc_session.group_sendmsg(
             { 'command': [ "shutdown" ] }, "StatsHttpd")
         self.stats_httpd.start()
-        self.stats_httpd = stats_httpd.StatsHttpd(self.verbose)
-        self.stats_httpd.cc_session.verbose = False
+        self.stats_httpd = stats_httpd.StatsHttpd()
         self.assertRaises(
             fake_select.error, self.stats_httpd.start)
 

+ 11 - 11
src/bin/stats/tests/b10-stats_test.py

@@ -31,18 +31,18 @@ stats.gmtime = gmtime
 from stats import SessionSubject, CCSessionListener, get_timestamp, get_datetime
 from fake_time import _TEST_TIME_SECS, _TEST_TIME_STRF
 
-# setting Constant
-if sys.path[0] == '':
-    TEST_SPECFILE_LOCATION = "./testdata/stats_test.spec"
+if "B10_FROM_SOURCE" in os.environ:
+    TEST_SPECFILE_LOCATION = os.environ["B10_FROM_SOURCE"] +\
+    "/src/bin/stats/tests/testdata/stats_test.spec"
 else:
-    TEST_SPECFILE_LOCATION = sys.path[0] + "/testdata/stats_test.spec"
+    TEST_SPECFILE_LOCATION = "./testdata/stats_test.spec"
 
 class TestStats(unittest.TestCase):
 
     def setUp(self):
         self.session = Session()
-        self.subject = SessionSubject(session=self.session, verbose=True)
-        self.listener = CCSessionListener(self.subject, verbose=True)
+        self.subject = SessionSubject(session=self.session)
+        self.listener = CCSessionListener(self.subject)
         self.stats_spec = self.listener.cc_session.get_module_spec().get_config_spec()
         self.module_name = self.listener.cc_session.get_module_spec().get_module_name()
         self.stats_data = {
@@ -516,9 +516,9 @@ class TestStats(unittest.TestCase):
 class TestStats2(unittest.TestCase):
 
     def setUp(self):
-        self.session = Session(verbose=True)
-        self.subject = SessionSubject(session=self.session, verbose=True)
-        self.listener = CCSessionListener(self.subject, verbose=True)
+        self.session = Session()
+        self.subject = SessionSubject(session=self.session)
+        self.listener = CCSessionListener(self.subject)
         self.module_name = self.listener.cc_session.get_module_spec().get_module_name()
         # check starting
         self.assertFalse(self.subject.running)
@@ -553,9 +553,9 @@ class TestStats2(unittest.TestCase):
         stats.SPECFILE_LOCATION = TEST_SPECFILE_LOCATION
         stats.SCHEMA_SPECFILE_LOCATION = TEST_SPECFILE_LOCATION
         self.assertEqual(stats.SPECFILE_LOCATION, TEST_SPECFILE_LOCATION)
-        self.subject = stats.SessionSubject(session=self.session, verbose=True)
+        self.subject = stats.SessionSubject(session=self.session)
         self.session = self.subject.session
-        self.listener = stats.CCSessionListener(self.subject, verbose=True)
+        self.listener = stats.CCSessionListener(self.subject)
 
         self.assertEqual(self.listener.stats_spec, [])
         self.assertEqual(self.listener.stats_data, {})

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

@@ -1,4 +1,4 @@
-SUBDIRS = cc config util
+SUBDIRS = cc config util log
 EXTRA_DIST = __init__.py
 CLEANFILES = __init__.pyc
 

+ 7 - 0
src/bin/stats/tests/isc/log/Makefile.am

@@ -0,0 +1,7 @@
+EXTRA_DIST = __init__.py
+CLEANFILES = __init__.pyc
+
+CLEANDIRS = __pycache__
+
+clean-local:
+	rm -rf $(CLEANDIRS)

+ 33 - 0
src/bin/stats/tests/isc/log/__init__.py

@@ -0,0 +1,33 @@
+# Copyright (C) 2011  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+# This file is not installed. The log.so is installed into the right place.
+# It is only to find it in the .libs directory when we run as a test or
+# from the build directory.
+# But as nobody gives us the builddir explicitly (and we can't use generation
+# from .in file, as it would put us into the builddir and we wouldn't be found)
+# we guess from current directory. Any idea for something better? This should
+# be enough for the tests, but would it work for B10_FROM_SOURCE as well?
+# Should we look there? Or define something in bind10_config?
+
+import os
+import sys
+
+for base in sys.path[:]:
+    loglibdir = os.path.join(base, 'isc/log/.libs')
+    if os.path.exists(loglibdir):
+        sys.path.insert(0, loglibdir)
+
+from log import *

+ 2 - 3
src/bin/xfrin/xfrin.py.in

@@ -152,7 +152,7 @@ class XfrinConnection(asyncore.dispatcher):
             self.connect(self._master_address)
             return True
         except socket.error as e:
-            logger.error(CONNECT_MASTER, self._master_address, str(e))
+            logger.error(XFRIN_CONNECT_MASTER, self._master_address, str(e))
             return False
 
     def _create_query(self, query_type):
@@ -548,8 +548,7 @@ class Xfrin:
         self._send_cc_session = isc.cc.Session()
         self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
                                               self.config_handler,
-                                              self.command_handler,
-                                              None, True)
+                                              self.command_handler)
         self._module_cc.start()
         config_data = self._module_cc.get_full_config()
         self.config_handler(config_data)

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

@@ -566,7 +566,7 @@ class XfroutServer:
         #self._log = None
         self._listen_sock_file = UNIX_SOCKET_FILE
         self._shutdown_event = threading.Event()
-        self._cc = isc.config.ModuleCCSession(SPECFILE_LOCATION, self.config_handler, self.command_handler, None, True)
+        self._cc = isc.config.ModuleCCSession(SPECFILE_LOCATION, self.config_handler, self.command_handler)
         self._config_data = self._cc.get_full_config()
         self._cc.start()
         self._cc.add_remote_config(AUTH_SPECFILE_LOCATION);

+ 2 - 2
src/lib/Makefile.am

@@ -1,3 +1,3 @@
-SUBDIRS = exceptions util log cryptolink dns cc config python xfr \
+SUBDIRS = exceptions util log cryptolink dns cc config acl python xfr \
           bench asiolink asiodns nsas cache resolve testutils datasrc \
-          acl server_common
+          server_common

+ 3 - 0
src/lib/acl/acl.h

@@ -88,8 +88,11 @@ public:
      * the context against conditions and if it matches, returns the
      * action that belongs to the first matched entry or default action
      * if nothing matches.
+     *
      * \param context The thing that should be checked. It is directly
      *     passed to the checks.
+     *
+     * \return The action for the ACL entry that first matches the context.
      */
     const Action& execute(const Context& context) const {
         const typename Entries::const_iterator end(entries_.end());

+ 15 - 15
src/lib/acl/loader.h

@@ -101,21 +101,21 @@ BasicAction defaultActionLoader(data::ConstElementPtr action);
  *
  * An ACL definition looks like this:
  * \verbatim
- * [
- *   {
- *      "action": "ACCEPT",
- *      "match-type": <parameter>
- *   },
- *   {
- *      "action": "REJECT",
- *      "match-type": <parameter>
- *      "another-match-type": [<parameter1>, <parameter2>]
-*    },
-*    {
-*       "action": "DROP"
-*    }
- * ]
- * \endverbatim
+ [
+   {
+      "action": "ACCEPT",
+      "match-type": <parameter>
+   },
+   {
+      "action": "REJECT",
+      "match-type": <parameter>,
+      "another-match-type": [<parameter1>, <parameter2>]
+   },
+   {
+      "action": "DROP"
+   }
+ ]
+ \endverbatim
  *
  * This is a list of elements. Each element must have an "action"
  * entry/keyword. That one specifies which action is returned if this

+ 21 - 42
src/lib/asiolink/tests/interval_timer_unittest.cc

@@ -28,7 +28,7 @@ const boost::posix_time::time_duration TIMER_MARGIN_MSEC =
 
 using namespace isc::asiolink;
 
-// This fixture is for testing IntervalTimer. Some callback functors are 
+// This fixture is for testing IntervalTimer. Some callback functors are
 // registered as callback function of the timer to test if they are called
 // or not.
 class IntervalTimerTest : public ::testing::Test {
@@ -50,7 +50,9 @@ protected:
     };
     class TimerCallBackCounter : public std::unary_function<void, void> {
     public:
-        TimerCallBackCounter(IntervalTimerTest* test_obj) : test_obj_(test_obj) {
+        TimerCallBackCounter(IntervalTimerTest* test_obj) :
+            test_obj_(test_obj)
+        {
             counter_ = 0;
         }
         void operator()() {
@@ -164,24 +166,20 @@ TEST_F(IntervalTimerTest, startIntervalTimer) {
     itimer.setup(TimerCallBack(this), 100);
     EXPECT_EQ(100, itimer.getInterval());
     io_service_.run();
-    // reaches here after timer expired
+    // Control reaches here after io_service_ was stopped by TimerCallBack.
+
     // delta: difference between elapsed time and 100 milliseconds.
     boost::posix_time::time_duration test_runtime =
         boost::posix_time::microsec_clock::universal_time() - start;
-    EXPECT_FALSE(test_runtime.is_negative()) << 
-                 "test duration " << test_runtime << 
+    EXPECT_FALSE(test_runtime.is_negative()) <<
+                 "test duration " << test_runtime <<
                  " negative - clock skew?";
-    boost::posix_time::time_duration delta =
-        test_runtime - boost::posix_time::milliseconds(100);
-    if (delta.is_negative()) {
-        delta.invert_sign();
-    }
-    // expect TimerCallBack is called; timer_called_ is true
+    // Expect TimerCallBack is called; timer_called_ is true
     EXPECT_TRUE(timer_called_);
-    // expect interval is 100 milliseconds +/- TIMER_MARGIN_MSEC.
-    EXPECT_TRUE(delta < TIMER_MARGIN_MSEC) << 
-                "delta " << delta.total_milliseconds() << "msec " <<
-                ">= " << TIMER_MARGIN_MSEC.total_milliseconds();
+    // Expect test_runtime is 100 milliseconds or longer.
+    EXPECT_TRUE(test_runtime > boost::posix_time::milliseconds(100)) <<
+                "test runtime " << test_runtime.total_milliseconds() <<
+                "msec " << ">= 100";
 }
 
 TEST_F(IntervalTimerTest, destructIntervalTimer) {
@@ -244,7 +242,7 @@ TEST_F(IntervalTimerTest, cancel) {
 }
 
 TEST_F(IntervalTimerTest, overwriteIntervalTimer) {
-    // Calling setup() multiple times updates call back function and interval.
+    // Call setup() multiple times to update call back function and interval.
     //
     // There are two timers:
     //  itimer (A)
@@ -266,7 +264,7 @@ TEST_F(IntervalTimerTest, overwriteIntervalTimer) {
     //     0  100  200  300  400  500  600  700  800 (ms)
     // (A) i-------------+----C----s
     //                        ^    ^stop io_service
-    //                        |change call back function
+    //                        |change call back function and interval
     // (B) i------------------+-------------------S
     //                                            ^(stop io_service on fail)
     //
@@ -279,30 +277,11 @@ TEST_F(IntervalTimerTest, overwriteIntervalTimer) {
     itimer.setup(TimerCallBackCounter(this), 300);
     itimer_overwriter.setup(TimerCallBackOverwriter(this, itimer), 400);
     io_service_.run();
-    // reaches here after timer expired
-    // if interval is updated, it takes
-    //   400 milliseconds for TimerCallBackOverwriter
-    //   + 100 milliseconds for TimerCallBack (stop)
-    //   = 500 milliseconds.
-    // otherwise (test fails), it takes
-    //   400 milliseconds for TimerCallBackOverwriter
-    //   + 400 milliseconds for TimerCallBackOverwriter (stop)
-    //   = 800 milliseconds.
-    // delta: difference between elapsed time and 400 + 100 milliseconds
-    boost::posix_time::time_duration test_runtime =
-        boost::posix_time::microsec_clock::universal_time() - start;
-    EXPECT_FALSE(test_runtime.is_negative()) << 
-                 "test duration " << test_runtime << 
-                 " negative - clock skew?";
-    boost::posix_time::time_duration delta =
-        test_runtime - boost::posix_time::milliseconds(400 + 100);
-    if (delta.is_negative()) {
-        delta.invert_sign();
-    }
-    // expect callback function is updated: TimerCallBack is called
+    // Control reaches here after io_service_ was stopped by
+    // TimerCallBackCounter or TimerCallBackOverwriter.
+
+    // Expect callback function is updated: TimerCallBack is called
     EXPECT_TRUE(timer_called_);
-    // expect interval is updated
-    EXPECT_TRUE(delta < TIMER_MARGIN_MSEC) << 
-                "delta " << delta.total_milliseconds() << " msec " <<
-                ">= " << TIMER_MARGIN_MSEC.total_milliseconds();
+    // Expect interval is updated: return value of getInterval() is updated
+    EXPECT_EQ(itimer.getInterval(), 100);
 }

+ 2 - 2
src/lib/config/ccsession.h

@@ -179,7 +179,7 @@ public:
      * We'll need to develop a cleaner solution, and then remove this knob)
      * @param handle_logging If true, the ModuleCCSession will automatically
      * take care of logging configuration through the virtual Logging config
-     * module.
+     * module. Defaults to true.
      */
     ModuleCCSession(const std::string& spec_file_name,
                     isc::cc::AbstractSession& session,
@@ -189,7 +189,7 @@ public:
                         const std::string& command,
                         isc::data::ConstElementPtr args) = NULL,
                     bool start_immediately = true,
-                    bool handle_logging = false
+                    bool handle_logging = true
                     );
 
     /// Start receiving new commands and configuration changes asynchronously.

+ 19 - 13
src/lib/config/tests/ccsession_unittests.cc

@@ -151,7 +151,8 @@ TEST_F(CCSessionTest, parseCommand) {
 
 TEST_F(CCSessionTest, session1) {
     EXPECT_FALSE(session.haveSubscription("Spec1", "*"));
-    ModuleCCSession mccs(ccspecfile("spec1.spec"), session, NULL, NULL);
+    ModuleCCSession mccs(ccspecfile("spec1.spec"), session, NULL, NULL,
+                         true, false);
     EXPECT_TRUE(session.haveSubscription("Spec1", "*"));
 
     EXPECT_EQ(1, session.getMsgQueue()->size());
@@ -163,14 +164,15 @@ TEST_F(CCSessionTest, session1) {
     EXPECT_EQ("*", to);
     EXPECT_EQ(0, session.getMsgQueue()->size());
 
-    // without explicit argument, the session should not automatically
+    // with this argument, the session should not automatically
     // subscribe to logging config
     EXPECT_FALSE(session.haveSubscription("Logging", "*"));
 }
 
 TEST_F(CCSessionTest, session2) {
     EXPECT_FALSE(session.haveSubscription("Spec2", "*"));
-    ModuleCCSession mccs(ccspecfile("spec2.spec"), session, NULL, NULL);
+    ModuleCCSession mccs(ccspecfile("spec2.spec"), session, NULL, NULL,
+                         true, false);
     EXPECT_TRUE(session.haveSubscription("Spec2", "*"));
 
     EXPECT_EQ(1, session.getMsgQueue()->size());
@@ -217,7 +219,7 @@ TEST_F(CCSessionTest, session3) {
 
     EXPECT_FALSE(session.haveSubscription("Spec2", "*"));
     ModuleCCSession mccs(ccspecfile("spec2.spec"), session, my_config_handler,
-                         my_command_handler);
+                         my_command_handler, true, false);
     EXPECT_TRUE(session.haveSubscription("Spec2", "*"));
 
     EXPECT_EQ(2, session.getMsgQueue()->size());
@@ -241,7 +243,7 @@ TEST_F(CCSessionTest, checkCommand) {
 
     EXPECT_FALSE(session.haveSubscription("Spec29", "*"));
     ModuleCCSession mccs(ccspecfile("spec29.spec"), session, my_config_handler,
-                         my_command_handler);
+                         my_command_handler, true, false);
     EXPECT_TRUE(session.haveSubscription("Spec29", "*"));
 
     EXPECT_EQ(2, session.getMsgQueue()->size());
@@ -318,7 +320,7 @@ TEST_F(CCSessionTest, checkCommand2) {
     session.getMessages()->add(createAnswer(0, el("{}")));
     EXPECT_FALSE(session.haveSubscription("Spec29", "*"));
     ModuleCCSession mccs(ccspecfile("spec29.spec"), session, my_config_handler,
-                         my_command_handler);
+                         my_command_handler, true, false);
     EXPECT_TRUE(session.haveSubscription("Spec29", "*"));
     ConstElementPtr msg;
     std::string group, to;
@@ -370,7 +372,8 @@ TEST_F(CCSessionTest, remoteConfig) {
     std::string module_name;
     int item1;
     
-    ModuleCCSession mccs(ccspecfile("spec1.spec"), session, NULL, NULL, false);
+    ModuleCCSession mccs(ccspecfile("spec1.spec"), session, NULL, NULL,
+                         false, false);
     EXPECT_TRUE(session.haveSubscription("Spec1", "*"));
     
     // first simply connect, with no config values, and see we get
@@ -526,7 +529,7 @@ TEST_F(CCSessionTest, ignoreRemoteConfigCommands) {
 
     EXPECT_FALSE(session.haveSubscription("Spec29", "*"));
     ModuleCCSession mccs(ccspecfile("spec29.spec"), session, my_config_handler,
-                         my_command_handler, false);
+                         my_command_handler, false, false);
     EXPECT_TRUE(session.haveSubscription("Spec29", "*"));
 
     EXPECT_EQ(2, session.getMsgQueue()->size());
@@ -578,14 +581,15 @@ TEST_F(CCSessionTest, initializationFail) {
 
 // Test it throws when we try to start it twice (once from the constructor)
 TEST_F(CCSessionTest, doubleStartImplicit) {
-    ModuleCCSession mccs(ccspecfile("spec29.spec"), session, NULL, NULL);
+    ModuleCCSession mccs(ccspecfile("spec29.spec"), session, NULL, NULL,
+                         true, false);
     EXPECT_THROW(mccs.start(), CCSessionError);
 }
 
 // The same, but both starts are explicit
 TEST_F(CCSessionTest, doubleStartExplicit) {
     ModuleCCSession mccs(ccspecfile("spec29.spec"), session, NULL, NULL,
-                         false);
+                         false, false);
     mccs.start();
     EXPECT_THROW(mccs.start(), CCSessionError);
 }
@@ -593,7 +597,8 @@ TEST_F(CCSessionTest, doubleStartExplicit) {
 // Test we can request synchronous receive before we start the session,
 // and check there's the mechanism if we do it after
 TEST_F(CCSessionTest, delayedStart) {
-    ModuleCCSession mccs(ccspecfile("spec2.spec"), session, NULL, NULL, false);
+    ModuleCCSession mccs(ccspecfile("spec2.spec"), session, NULL, NULL,
+                         false, false);
     session.getMessages()->add(createAnswer());
     ConstElementPtr env, answer;
     EXPECT_NO_THROW(session.group_recvmsg(env, answer, false, 3));
@@ -620,7 +625,7 @@ TEST_F(CCSessionTest, loggingStartBadSpec) {
     // just give an empty config
     session.getMessages()->add(createAnswer(0, el("{}")));
     EXPECT_THROW(new ModuleCCSession(ccspecfile("spec2.spec"), session,
-                 NULL, NULL, true, true), ModuleSpecError);
+                 NULL, NULL), ModuleSpecError);
     EXPECT_FALSE(session.haveSubscription("Logging", "*"));
 }
 
@@ -629,7 +634,8 @@ TEST_F(CCSessionTest, loggingStartBadSpec) {
 // if we need to call addRemoteConfig().
 // The correct cases are covered in remoteConfig test.
 TEST_F(CCSessionTest, doubleStartWithAddRemoteConfig) {
-    ModuleCCSession mccs(ccspecfile("spec29.spec"), session, NULL, NULL);
+    ModuleCCSession mccs(ccspecfile("spec29.spec"), session, NULL, NULL,
+                         true, false);
     session.getMessages()->add(createAnswer(0, el("{}")));
     EXPECT_THROW(mccs.addRemoteConfig(ccspecfile("spec2.spec")),
                  FakeSession::DoubleRead);

+ 2 - 1
src/lib/datasrc/cache.cc

@@ -232,7 +232,8 @@ HotCacheImpl::insert(const CacheNodePtr node) {
     if (iter != map_.end()) {
         CacheNodePtr old = iter->second;
         if (old && old->isValid()) {
-            LOG_DEBUG(logger, DBG_TRACE_DATA, DATASRC_CACHE_OLD_FOUND);
+            LOG_DEBUG(logger, DBG_TRACE_DATA, DATASRC_CACHE_OLD_FOUND)
+                      .arg(node->getNodeName());
             remove(old);
         }
     }

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

@@ -903,7 +903,7 @@ tryWildcard(Query& q, QueryTaskPtr task, ZoneInfo& zoneinfo, bool& found) {
             result = proveNX(q, task, zoneinfo, true);
             if (result != DataSrc::SUCCESS) {
                 m.setRcode(Rcode::SERVFAIL());
-                logger.error(DATASRC_QUERY_WILDCARD_PROVENX_FAIL).
+                logger.error(DATASRC_QUERY_WILDCARD_PROVE_NX_FAIL).
                     arg(task->qname).arg(result);
                 return (DataSrc::ERROR);
             }
@@ -1162,7 +1162,7 @@ DataSrc::doQuery(Query& q) {
                 result = proveNX(q, task, zoneinfo, false);
                 if (result != DataSrc::SUCCESS) {
                     m.setRcode(Rcode::SERVFAIL());
-                    logger.error(DATASRC_QUERY_PROVENX_FAIL).arg(task->qname);
+                    logger.error(DATASRC_QUERY_PROVE_NX_FAIL).arg(task->qname);
                     return;
                 }
             }

+ 66 - 60
src/lib/datasrc/datasrc_messages.mes

@@ -17,63 +17,63 @@ $NAMESPACE isc::datasrc
 # \brief Messages for the data source library
 
 % DATASRC_CACHE_CREATE creating the hotspot cache
-Debug information that the hotspot cache was created at startup.
+This is a debug message issued during startup when the hotspot cache
+is created.
 
 % DATASRC_CACHE_DESTROY destroying the hotspot cache
 Debug information. The hotspot cache is being destroyed.
 
-% DATASRC_CACHE_DISABLE disabling the cache
-The hotspot cache is disabled from now on. It is not going to store
-information or return anything.
+% DATASRC_CACHE_DISABLE disabling the hotspot cache
+A debug message issued when the hotspot cache is disabled.
 
-% DATASRC_CACHE_ENABLE enabling the cache
-The hotspot cache is enabled from now on.
+% DATASRC_CACHE_ENABLE enabling the hotspot cache
+A debug message issued when the hotspot cache is enabled.
 
-% DATASRC_CACHE_EXPIRED the item '%1' is expired
-Debug information. There was an attempt to look up an item in the hotspot
-cache. And the item was actually there, but it was too old, so it was removed
-instead and nothing is reported (the external behaviour is the same as with
-CACHE_NOT_FOUND).
+% DATASRC_CACHE_EXPIRED item '%1' in the hotspot cache has expired
+A debug message issued when a hotspot cache lookup located the item but it
+had expired.  The item was removed and the program proceeded as if the item
+had not been found.
 
 % DATASRC_CACHE_FOUND the item '%1' was found
-Debug information. An item was successfully looked up in the hotspot cache.
+Debug information. An item was successfully located in the hotspot cache.
 
-% DATASRC_CACHE_FULL cache is full, dropping oldest
+% DATASRC_CACHE_FULL hotspot cache is full, dropping oldest
 Debug information. After inserting an item into the hotspot cache, the
 maximum number of items was exceeded, so the least recently used item will
 be dropped. This should be directly followed by CACHE_REMOVE.
 
-% DATASRC_CACHE_INSERT inserting item '%1' into the cache
-Debug information. It means a new item is being inserted into the hotspot
+% DATASRC_CACHE_INSERT inserting item '%1' into the hotspot cache
+A debug message indicating that a new item is being inserted into the hotspot
 cache.
 
-% DATASRC_CACHE_NOT_FOUND the item '%1' was not found
-Debug information. It was attempted to look up an item in the hotspot cache,
-but it is not there.
+% DATASRC_CACHE_NOT_FOUND the item '%1' was not found in the hotspot cache
+A debug message issued when hotspot cache was searched for the specified
+item but it was not found.
 
-% DATASRC_CACHE_OLD_FOUND older instance of cache item found, replacing
+% DATASRC_CACHE_OLD_FOUND older instance of hotspot cache item '%1' found, replacing
 Debug information. While inserting an item into the hotspot cache, an older
-instance of an item with the same name was found. The old instance will be
-removed. This should be directly followed by CACHE_REMOVE.
+instance of an item with the same name was found; the old instance will be
+removed. This will be directly followed by CACHE_REMOVE.
 
-% DATASRC_CACHE_REMOVE removing '%1' from the cache
+% DATASRC_CACHE_REMOVE removing '%1' from the hotspot cache
 Debug information. An item is being removed from the hotspot cache.
 
-% DATASRC_CACHE_SLOTS setting the cache size to '%1', dropping '%2' items
+% DATASRC_CACHE_SLOTS setting the hotspot cache size to '%1', dropping '%2' items
 The maximum allowed number of items of the hotspot cache is set to the given
 number. If there are too many, some of them will be dropped. The size of 0
 means no limit.
 
 % DATASRC_DO_QUERY handling query for '%1/%2'
-Debug information. We're processing some internal query for given name and
-type.
+A debug message indicating that a query for the given name and RR type is being
+processed.
 
 % DATASRC_MEM_ADD_RRSET adding RRset '%1/%2' into zone '%3'
 Debug information. An RRset is being added to the in-memory data source.
 
 % DATASRC_MEM_ADD_WILDCARD adding wildcards for '%1'
-Debug information. Some special marks above each * in wildcard name are needed.
-They are being added now for this name.
+This is a debug message issued during the processing of a wildcard
+name. The internal domain name tree is scanned and some nodes are
+specially marked to allow the wildcard lookup to succeed.
 
 % DATASRC_MEM_ADD_ZONE adding zone '%1/%2'
 Debug information. A zone is being added into the in-memory data source.
@@ -114,9 +114,9 @@ stop the search.
 Debug information. A DNAME was found instead of the requested information.
 
 % DATASRC_MEM_DNAME_NS DNAME and NS can't coexist in non-apex domain '%1'
-It was requested for DNAME and NS records to be put into the same domain
-which is not the apex (the top of the zone). This is forbidden by RFC
-2672, section 3. This indicates a problem with provided data.
+A request was made for DNAME and NS records to be put into the same
+domain which is not the apex (the top of the zone). This is forbidden
+by RFC 2672 (section 3) and indicates a problem with provided data.
 
 % DATASRC_MEM_DOMAIN_EMPTY requested domain '%1' is empty
 Debug information. The requested domain exists in the tree of domains, but
@@ -142,7 +142,7 @@ in-memory data source.
 % DATASRC_MEM_LOAD loading zone '%1' from file '%2'
 Debug information. The content of master file is being loaded into the memory.
 
-% DATASRC_MEM_NOTFOUND requested domain '%1' not found
+% DATASRC_MEM_NOT_FOUND requested domain '%1' not found
 Debug information. The requested domain does not exist.
 
 % DATASRC_MEM_NS_ENCOUNTERED encountered a NS
@@ -201,11 +201,11 @@ behave and BIND 9 refuses that as well. Please describe your intention using
 different tools.
 
 % DATASRC_META_ADD adding a data source into meta data source
-Debug information. Yet another data source is being added into the meta data
-source. (probably at startup or reconfiguration)
+This is a debug message issued during startup or reconfiguration.
+Another data source is being added into the meta data source.
 
 % DATASRC_META_ADD_CLASS_MISMATCH mismatch between classes '%1' and '%2'
-It was attempted to add a data source into a meta data source. But their
+It was attempted to add a data source into a meta data source, but their
 classes do not match.
 
 % DATASRC_META_REMOVE removing data source from meta data source
@@ -234,11 +234,11 @@ specific error already.
 The domain lives in another zone. But it is not possible to generate referral
 information for it.
 
-% DATASRC_QUERY_CACHED data for %1/%2 found in cache
+% DATASRC_QUERY_CACHED data for %1/%2 found in hotspot cache
 Debug information. The requested data were found in the hotspot cache, so
 no query is sent to the real data source.
 
-% DATASRC_QUERY_CHECK_CACHE checking cache for '%1/%2'
+% DATASRC_QUERY_CHECK_CACHE checking hotspot cache for '%1/%2'
 Debug information. While processing a query, lookup to the hotspot cache
 is being made.
 
@@ -251,10 +251,9 @@ Debug information. The software is trying to identify delegation points on the
 way down to the given domain.
 
 % DATASRC_QUERY_EMPTY_CNAME CNAME at '%1' is empty
-There was an CNAME and it was being followed. But it contains no records,
-so there's nowhere to go. There will be no answer. This indicates a problem
-with supplied data.
-We tried to follow
+A CNAME chain was being followed and an entry was found that pointed
+to a domain name that had no RRsets associated with it. As a result,
+the query cannot be answered. This indicates a problem with supplied data.
 
 % DATASRC_QUERY_EMPTY_DNAME the DNAME on '%1' is empty
 During an attempt to synthesize CNAME from this DNAME it was discovered the
@@ -262,11 +261,11 @@ DNAME is empty (it has no records). This indicates problem with supplied data.
 
 % DATASRC_QUERY_FAIL query failed
 Some subtask of query processing failed. The reason should have been reported
-already. We are returning SERVFAIL.
+already and a SERVFAIL will be returned to the querying system.
 
 % DATASRC_QUERY_FOLLOW_CNAME following CNAME at '%1'
-Debug information. The domain is a CNAME (or a DNAME and we created a CNAME
-for it already), so it's being followed.
+Debug information. The domain is a CNAME (or a DNAME and a CNAME for it
+has already been created) and the search is following this chain.
 
 % DATASRC_QUERY_GET_MX_ADDITIONAL addition of A/AAAA for '%1' requested by MX '%2'
 Debug information. While processing a query, a MX record was met. It
@@ -291,14 +290,14 @@ operation code.
 Debug information. The last DO_QUERY is an auth query.
 
 % DATASRC_QUERY_IS_GLUE glue query (%1/%2)
-Debug information. The last DO_QUERY is query for glue addresses.
+Debug information. The last DO_QUERY is a query for glue addresses.
 
 % DATASRC_QUERY_IS_NOGLUE query for non-glue addresses (%1/%2)
-Debug information. The last DO_QUERY is query for addresses that are not
+Debug information. The last DO_QUERY is a query for addresses that are not
 glue.
 
 % DATASRC_QUERY_IS_REF query for referral (%1/%2)
-Debug information. The last DO_QUERY is query for referral information.
+Debug information. The last DO_QUERY is a query for referral information.
 
 % DATASRC_QUERY_IS_SIMPLE simple query (%1/%2)
 Debug information. The last DO_QUERY is a simple query.
@@ -322,11 +321,11 @@ The underlying data source failed to answer the no-glue query. 1 means some
 error, 2 is not implemented. The data source should have logged the specific
 error already.
 
-% DATASRC_QUERY_NO_CACHE_ANY_AUTH ignoring cache for ANY query (%1/%2 in %3 class)
+% DATASRC_QUERY_NO_CACHE_ANY_AUTH ignoring hotspot cache for ANY query (%1/%2 in %3 class)
 Debug information. The hotspot cache is ignored for authoritative ANY queries
 for consistency reasons.
 
-% DATASRC_QUERY_NO_CACHE_ANY_SIMPLE ignoring cache for ANY query (%1/%2 in %3 class)
+% DATASRC_QUERY_NO_CACHE_ANY_SIMPLE ignoring hotspot cache for ANY query (%1/%2 in %3 class)
 Debug information. The hotspot cache is ignored for ANY queries for consistency
 reasons.
 
@@ -345,7 +344,7 @@ domain. Maybe someone sent a query to the wrong server for some reason.
 % DATASRC_QUERY_PROCESS processing query '%1/%2' in the '%3' class
 Debug information. A sure query is being processed now.
 
-% DATASRC_QUERY_PROVENX_FAIL unable to prove nonexistence of '%1'
+% DATASRC_QUERY_PROVE_NX_FAIL unable to prove nonexistence of '%1'
 The user wants DNSSEC and we discovered the entity doesn't exist (either
 domain or the record). But there was an error getting NSEC/NSEC3 record
 to prove the nonexistence.
@@ -365,9 +364,9 @@ error, 2 is not implemented. The data source should have logged the specific
 error already.
 
 % DATASRC_QUERY_SYNTH_CNAME synthesizing CNAME from DNAME on '%1'
-Debug information. While answering a query, a DNAME was met. The DNAME itself
-will be returned, but along with it a CNAME for clients which don't understand
-DNAMEs will be synthesized.
+This is a debug message. While answering a query, a DNAME was encountered. The
+DNAME itself will be returned, along with a synthesized CNAME for clients that
+do not understand the DNAME RR.
 
 % DATASRC_QUERY_TASK_FAIL task failed with %1
 The query subtask failed. The reason should have been reported by the subtask
@@ -391,7 +390,7 @@ domain is being looked for now.
 During an attempt to cover the domain by a wildcard an error happened. The
 exact kind was hopefully already reported.
 
-% DATASRC_QUERY_WILDCARD_PROVENX_FAIL unable to prove nonexistence of '%1' (%2)
+% DATASRC_QUERY_WILDCARD_PROVE_NX_FAIL unable to prove nonexistence of '%1' (%2)
 While processing a wildcard, it wasn't possible to prove nonexistence of the
 given domain or record.  The code is 1 for error and 2 for not implemented.
 
@@ -411,7 +410,7 @@ Debug information. An instance of SQLite data source is being destroyed.
 Debug information. The SQLite data source is trying to identify which zone
 should hold this domain.
 
-% DATASRC_SQLITE_ENCLOSURE_NOTFOUND no zone contains it
+% DATASRC_SQLITE_ENCLOSURE_NOT_FOUND no zone contains '%1'
 Debug information. The last SQLITE_ENCLOSURE query was unsuccessful; there's
 no such zone in our data.
 
@@ -464,20 +463,27 @@ Debug information. The SQLite data source is loading an SQLite database in
 the provided file.
 
 % DATASRC_SQLITE_PREVIOUS looking for name previous to '%1'
-Debug information. We're trying to look up name preceding the supplied one.
+This is a debug message.  The name given was not found, so the program
+is searching for the next name higher up the hierarchy (e.g. if
+www.example.com were queried for and not found, the software searches
+for the "previous" name, example.com).
 
 % DATASRC_SQLITE_PREVIOUS_NO_ZONE no zone containing '%1'
-The SQLite data source tried to identify name preceding this one. But this
-one is not contained in any zone in the data source.
+The name given was not found, so the program is searching for the next
+name higher up the hierarchy (e.g. if www.example.com were queried
+for and not found, the software searches for the "previous" name,
+example.com). However, this name is not contained in any zone in the
+data source. This is an error since it indicates a problem in the earlier
+processing of the query.
 
 % DATASRC_SQLITE_SETUP setting up SQLite database
 The database for SQLite data source was found empty. It is assumed this is the
 first run and it is being initialized with current schema.  It'll still contain
 no data, but it will be ready for use.
 
-% DATASRC_STATIC_BAD_CLASS static data source can handle CH only
-For some reason, someone asked the static data source a query that is not in
-the CH class.
+% DATASRC_STATIC_CLASS_NOT_CH static data source can handle CH class only
+An error message indicating that a query requesting a RR for a class other
+that CH was sent to the static data source (which only handles CH queries).
 
 % DATASRC_STATIC_CREATE creating the static datasource
 Debug information. The static data source (the one holding stuff like

+ 2 - 2
src/lib/datasrc/memory_datasrc.cc

@@ -129,7 +129,7 @@ struct MemoryZone::MemoryZoneImpl {
         // Ensure CNAME and other type of RR don't coexist for the same
         // owner name.
         if (rrset->getType() == RRType::CNAME()) {
-            // XXX: this check will become incorrect when we support DNSSEC
+            // TODO: this check will become incorrect when we support DNSSEC
             // (depending on how we support DNSSEC).  We should revisit it
             // at that point.
             if (!domain->empty()) {
@@ -523,7 +523,7 @@ struct MemoryZone::MemoryZoneImpl {
 
                 // fall through
             case DomainTree::NOTFOUND:
-                LOG_DEBUG(logger, DBG_TRACE_DATA, DATASRC_MEM_NOTFOUND).
+                LOG_DEBUG(logger, DBG_TRACE_DATA, DATASRC_MEM_NOT_FOUND).
                     arg(name);
                 return (FindResult(NXDOMAIN, ConstRRsetPtr()));
             case DomainTree::EXACTMATCH: // This one is OK, handle it

+ 2 - 1
src/lib/datasrc/sqlite3_datasrc.cc

@@ -356,7 +356,8 @@ Sqlite3DataSrc::findClosestEnclosure(DataSrcMatch& match) const {
 
     unsigned int position;
     if (findClosest(match.getName(), &position) == -1) {
-        LOG_DEBUG(logger, DBG_TRACE_DATA, DATASRC_SQLITE_ENCLOSURE_NOTFOUND);
+        LOG_DEBUG(logger, DBG_TRACE_DATA, DATASRC_SQLITE_ENCLOSURE_NOT_FOUND)
+                  .arg(match.getName());
         return;
     }
 

+ 1 - 1
src/lib/datasrc/static_datasrc.cc

@@ -161,7 +161,7 @@ StaticDataSrc::findRRset(const Name& qname,
         arg(qtype);
     flags = 0;
     if (qclass != getClass() && qclass != RRClass::ANY()) {
-        LOG_ERROR(logger, DATASRC_STATIC_BAD_CLASS);
+        LOG_ERROR(logger, DATASRC_STATIC_CLASS_NOT_CH);
         return (ERROR);
     }
 

+ 1 - 1
src/lib/dns/tests/tsig_unittest.cc

@@ -440,7 +440,7 @@ TEST_F(TSIGTest, signUsingHMACSHA224) {
         0xef, 0x33, 0xa2, 0xda, 0xa1, 0x48, 0x71, 0xd3
     };
     {
-        SCOPED_TRACE("Sign test using HMAC-SHA1");
+        SCOPED_TRACE("Sign test using HMAC-SHA224");
         commonSignChecks(createMessageAndSign(sha1_qid, test_name, &sha1_ctx),
                          sha1_qid, 0x4dae7d5f, expected_mac,
                          sizeof(expected_mac), 0, 0, NULL,

+ 65 - 27
src/lib/log/compiler/message.cc

@@ -43,6 +43,7 @@ using namespace isc::util;
 
 static const char* VERSION = "1.0-0";
 
+/// \file log/compiler/message.cc
 /// \brief Message Compiler
 ///
 /// \b Overview<BR>
@@ -55,13 +56,16 @@ static const char* VERSION = "1.0-0";
 /// \b Invocation<BR>
 /// The program is invoked with the command:
 ///
-/// <tt>message [-v | -h | \<message-file\>]</tt>
+/// <tt>message [-v | -h | -p | -d <dir> | <message-file>]</tt>
 ///
-/// It reads the message file and writes out two files of the same name in the
-/// default directory but with extensions of .h and .cc.
+/// It reads the message file and writes out two files of the same
+/// name in the current working directory (unless -d is used) but
+/// with extensions of .h and .cc, or .py if -p is used.
 ///
-/// \-v causes it to print the version number and exit. \-h prints a help
-/// message (and exits).
+/// -v causes it to print the version number and exit. -h prints a help
+/// message (and exits). -p sets the output to python. -d <dir> will make
+/// it write the output file(s) to dir instead of current working
+/// directory
 
 
 /// \brief Print Version
@@ -80,11 +84,12 @@ version() {
 void
 usage() {
     cout <<
-        "Usage: message [-h] [-v] [-p] <message-file>\n" <<
+        "Usage: message [-h] [-v] [-p] [-d dir] <message-file>\n" <<
         "\n" <<
         "-h       Print this message and exit\n" <<
         "-v       Print the program version and exit\n" <<
         "-p       Output python source instead of C++ ones\n" <<
+        "-d <dir> Place output files in given directory\n" <<
         "\n" <<
         "<message-file> is the name of the input message file.\n";
 }
@@ -106,7 +111,7 @@ currentTime() {
 
     // Convert to string and strip out the trailing newline
     string current_time = buffer;
-    return isc::util::str::trim(current_time);
+    return (isc::util::str::trim(current_time));
 }
 
 
@@ -127,7 +132,7 @@ sentinel(Filename& file) {
     string ext = file.extension();
     string sentinel_text = "__" + name + "_" + ext.substr(1);
     isc::util::str::uppercase(sentinel_text);
-    return sentinel_text;
+    return (sentinel_text);
 }
 
 
@@ -154,7 +159,7 @@ quoteString(const string& instring) {
         outstring += instring[i];
     }
 
-    return outstring;
+    return (outstring);
 }
 
 
@@ -177,7 +182,7 @@ sortedIdentifiers(MessageDictionary& dictionary) {
     }
     sort(ident.begin(), ident.end());
 
-    return ident;
+    return (ident);
 }
 
 
@@ -207,7 +212,7 @@ splitNamespace(string ns) {
 
     // ... and return the vector of namespace components split on the single
     // colon.
-    return isc::util::str::tokens(ns, ":");
+    return (isc::util::str::tokens(ns, ":"));
 }
 
 
@@ -249,14 +254,22 @@ writeClosingNamespace(ostream& output, const vector<string>& ns) {
 /// \param file Name of the message file. The source code is written to a file
 ///     file of the same name but with a .py suffix.
 /// \param dictionary The dictionary holding the message definitions.
+/// \param output_directory if not null NULL, output files are written
+///     to the given directory. If NULL, they are written to the current
+///     working directory.
 ///
 /// \note We don't use the namespace as in C++. We don't need it, because
 ///     python file/module works as implicit namespace as well.
 
 void
-writePythonFile(const string& file, MessageDictionary& dictionary) {
+writePythonFile(const string& file, MessageDictionary& dictionary,
+                const char* output_directory)
+{
     Filename message_file(file);
     Filename python_file(Filename(message_file.name()).useAsDefault(".py"));
+    if (output_directory != NULL) {
+        python_file.setDirectory(output_directory);
+    }
 
     // Open the file for writing
     ofstream pyfile(python_file.fullName().c_str());
@@ -291,13 +304,19 @@ writePythonFile(const string& file, MessageDictionary& dictionary) {
 /// \param ns Namespace in which the definitions are to be placed.  An empty
 /// string indicates no namespace.
 /// \param dictionary Dictionary holding the message definitions.
+/// \param output_directory if not null NULL, output files are written
+///     to the given directory. If NULL, they are written to the current
+///     working directory.
 
 void
 writeHeaderFile(const string& file, const vector<string>& ns_components,
-                MessageDictionary& dictionary)
+                MessageDictionary& dictionary, const char* output_directory)
 {
     Filename message_file(file);
     Filename header_file(Filename(message_file.name()).useAsDefault(".h"));
+    if (output_directory != NULL) {
+        header_file.setDirectory(output_directory);
+    }
 
     // Text to use as the sentinels.
     string sentinel_text = sentinel(header_file);
@@ -382,13 +401,25 @@ replaceNonAlphaNum(char c) {
 /// optimisation is done at link-time, not compiler-time.  In this it _may_
 /// decide to remove the initializer object because of a lack of references
 /// to it.  But until BIND-10 is ported to Windows, we won't know.
-
+///
+/// \param file Name of the message file.  The header file is written to a
+/// file of the same name but with a .h suffix.
+/// \param ns Namespace in which the definitions are to be placed.  An empty
+/// string indicates no namespace.
+/// \param dictionary Dictionary holding the message definitions.
+/// \param output_directory if not null NULL, output files are written
+///     to the given directory. If NULL, they are written to the current
+///     working directory.
 void
 writeProgramFile(const string& file, const vector<string>& ns_components,
-                 MessageDictionary& dictionary)
+                 MessageDictionary& dictionary,
+                 const char* output_directory)
 {
     Filename message_file(file);
     Filename program_file(Filename(message_file.name()).useAsDefault(".cc"));
+    if (output_directory) {
+        program_file.setDirectory(output_directory);
+    }
 
     // Open the output file for writing
     ofstream ccfile(program_file.fullName().c_str());
@@ -496,30 +527,35 @@ warnDuplicates(MessageReader& reader) {
 int
 main(int argc, char* argv[]) {
 
-    const char* soptions = "hvp";               // Short options
+    const char* soptions = "hvpd:";               // Short options
 
     optind = 1;             // Ensure we start a new scan
     int  opt;               // Value of the option
 
     bool doPython = false;
+    const char *output_directory = NULL;
 
     while ((opt = getopt(argc, argv, soptions)) != -1) {
         switch (opt) {
+            case 'd':
+                output_directory = optarg;
+                break;
+
             case 'p':
                 doPython = true;
                 break;
 
             case 'h':
                 usage();
-                return 0;
+                return (0);
 
             case 'v':
                 version();
-                return 0;
+                return (0);
 
             default:
                 // A message will have already been output about the error.
-                return 1;
+                return (1);
         }
     }
 
@@ -527,11 +563,11 @@ main(int argc, char* argv[]) {
     if (optind < (argc - 1)) {
         cout << "Error: excess arguments in command line\n";
         usage();
-        return 1;
+        return (1);
     } else if (optind >= argc) {
         cout << "Error: missing message file\n";
         usage();
-        return 1;
+        return (1);
     }
     string message_file = argv[optind];
 
@@ -552,7 +588,7 @@ main(int argc, char* argv[]) {
             }
 
             // Write the whole python file
-            writePythonFile(message_file, dictionary);
+            writePythonFile(message_file, dictionary, output_directory);
         } else {
             // Get the namespace into which the message definitions will be put and
             // split it into components.
@@ -560,16 +596,18 @@ main(int argc, char* argv[]) {
                 splitNamespace(reader.getNamespace());
 
             // Write the header file.
-            writeHeaderFile(message_file, ns_components, dictionary);
+            writeHeaderFile(message_file, ns_components, dictionary,
+                            output_directory);
 
             // Write the file that defines the message symbols and text
-            writeProgramFile(message_file, ns_components, dictionary);
+            writeProgramFile(message_file, ns_components, dictionary,
+                             output_directory);
         }
 
         // Finally, warn of any duplicates encountered.
         warnDuplicates(reader);
     }
-    catch (MessageException& e) {
+    catch (const MessageException& e) {
         // Create an error message from the ID and the text
         MessageDictionary& global = MessageDictionary::globalDictionary();
         string text = e.id();
@@ -583,9 +621,9 @@ main(int argc, char* argv[]) {
 
         cerr << text << "\n";
 
-        return 1;
+        return (1);
     }
 
-    return 0;
+    return (0);
 
 }

+ 31 - 1
src/lib/log/log_formatter.h

@@ -18,12 +18,28 @@
 #include <cstddef>
 #include <string>
 #include <iostream>
+
+#include <exceptions/exceptions.h>
 #include <boost/lexical_cast.hpp>
 #include <log/logger_level.h>
 
 namespace isc {
 namespace log {
 
+/// \brief Format Failure
+///
+/// This exception is used to wrap a bad_lexical_cast exception thrown during
+/// formatting an argument.
+
+class FormatFailure : public isc::Exception {
+public:
+    FormatFailure(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what)
+    {}
+};
+
+
+///
 /// \brief The internal replacement routine
 ///
 /// This is used internally by the Formatter. Replaces a placeholder
@@ -156,7 +172,21 @@ public:
     /// \param arg The argument to place into the placeholder.
     template<class Arg> Formatter& arg(const Arg& value) {
         if (logger_) {
-            return (arg(boost::lexical_cast<std::string>(value)));
+            try {
+                return (arg(boost::lexical_cast<std::string>(value)));
+            } catch (const boost::bad_lexical_cast& ex) {
+
+                // A bad_lexical_cast during a conversion to a string is
+                // *extremely* unlikely to fail.  However, there is nothing
+                // in the documentation that rules it out, so we need to handle
+                // it.  As it is a potentially very serious problem, throw the
+                // exception detailing the problem with as much information as
+                // we can.  (Note that this does not include 'value' -
+                // boost::lexical_cast failed to convert it to a string, so an
+                // attempt to do so here would probably fail as well.)
+                isc_throw(FormatFailure, "bad_lexical_cast in call to "
+                          "Formatter::arg(): " << ex.what());
+            }
         } else {
             return (*this);
         }

+ 2 - 0
src/lib/log/tests/Makefile.am

@@ -51,6 +51,7 @@ logger_example_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 logger_example_LDFLAGS = $(AM_LDFLAGS) $(LOG4CPLUS_LDFLAGS)
 logger_example_LDADD  = $(top_builddir)/src/lib/log/liblog.la
 logger_example_LDADD += $(top_builddir)/src/lib/util/libutil.la
+logger_example_LDADD += $(top_builddir)/src/lib/exceptions/libexceptions.la
 
 check_PROGRAMS += init_logger_test
 init_logger_test_SOURCES = init_logger_test.cc
@@ -58,6 +59,7 @@ init_logger_test_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 init_logger_test_LDFLAGS = $(AM_LDFLAGS) $(LOG4CPLUS_LDFLAGS)
 init_logger_test_LDADD  = $(top_builddir)/src/lib/log/liblog.la
 init_logger_test_LDADD += $(top_builddir)/src/lib/util/libutil.la
+init_logger_test_LDADD += $(top_builddir)/src/lib/exceptions/libexceptions.la
 
 noinst_PROGRAMS = $(TESTS)
 

+ 1 - 1
src/lib/python/isc/Makefile.am

@@ -1,4 +1,4 @@
-SUBDIRS = datasrc cc config log net notify util testutils
+SUBDIRS = datasrc cc config log net notify util testutils acl
 
 python_PYTHON = __init__.py
 

+ 45 - 0
src/lib/python/isc/acl/Makefile.am

@@ -0,0 +1,45 @@
+SUBDIRS = . tests
+
+AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
+AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CXXFLAGS = $(B10_CXXFLAGS)
+
+python_PYTHON = __init__.py
+pythondir = $(PYTHON_SITEPKG_DIR)/isc/acl
+
+pyexec_LTLIBRARIES = acl.la dns.la
+pyexecdir = $(PYTHON_SITEPKG_DIR)/isc/acl
+
+acl_la_SOURCES = acl.cc
+acl_la_CPPFLAGS = $(AM_CPPFLAGS) $(PYTHON_INCLUDES)
+acl_la_LDFLAGS = $(PYTHON_LDFLAGS)
+acl_la_CXXFLAGS = $(AM_CXXFLAGS) $(PYTHON_CXXFLAGS)
+
+dns_la_SOURCES = dns.h dns.cc dns_requestacl_python.h dns_requestacl_python.cc
+dns_la_SOURCES += dns_requestcontext_python.h dns_requestcontext_python.cc
+dns_la_SOURCES += dns_requestloader_python.h dns_requestloader_python.cc
+dns_la_CPPFLAGS = $(AM_CPPFLAGS) $(PYTHON_INCLUDES)
+dns_la_LDFLAGS = $(PYTHON_LDFLAGS)
+# Note: PYTHON_CXXFLAGS may have some -Wno... workaround, which must be
+# placed after -Wextra defined in AM_CXXFLAGS
+dns_la_CXXFLAGS = $(AM_CXXFLAGS) $(PYTHON_CXXFLAGS)
+
+# Python prefers .so, while some OSes (specifically MacOS) use a different
+# suffix for dynamic objects.  -module is necessary to work this around.
+acl_la_LDFLAGS += -module
+acl_la_LIBADD = $(top_builddir)/src/lib/acl/libacl.la
+acl_la_LIBADD += $(PYTHON_LIB)
+
+dns_la_LDFLAGS += -module
+dns_la_LIBADD = $(top_builddir)/src/lib/acl/libdnsacl.la
+dns_la_LIBADD += $(PYTHON_LIB)
+
+EXTRA_DIST = acl.py dns.py
+EXTRA_DIST += acl_inc.cc
+EXTRA_DIST += dnsacl_inc.cc dns_requestacl_inc.cc dns_requestcontext_inc.cc
+EXTRA_DIST += dns_requestloader_inc.cc
+
+CLEANDIRS = __pycache__
+
+clean-local:
+	rm -rf $(CLEANDIRS)

+ 11 - 0
src/lib/python/isc/acl/__init__.py

@@ -0,0 +1,11 @@
+"""
+Here are function and classes for manipulating access control lists.
+"""
+
+# The DNS ACL loader would need the json module.  Make sure it's imported
+# beforehand.
+import json
+
+# Other ACL modules highly depends on the main acl sub module, so it's
+# explicitly imported here.
+import isc.acl.acl

+ 80 - 0
src/lib/python/isc/acl/acl.cc

@@ -0,0 +1,80 @@
+// Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <Python.h>
+
+#include <util/python/pycppwrapper_util.h>
+
+#include <acl/acl.h>
+
+using namespace isc::util::python;
+
+#include "acl_inc.cc"
+
+namespace {
+// Commonly used Python exception objects.  Right now the acl module consists
+// of only one .cc file, so we hide them in an unnamed namespace.  If and when
+// we extend this module with multiple .cc files, we should move them to
+// a named namespace, say isc::acl::python, and declare them in a separate
+// header file.
+PyObject* po_ACLError;
+PyObject* po_LoaderError;
+}
+
+namespace {
+PyModuleDef acl = {
+    { PyObject_HEAD_INIT(NULL) NULL, 0, NULL},
+    "isc.acl.acl",
+    acl_doc,
+    -1,
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    NULL
+};
+} // end of unnamed namespace
+
+PyMODINIT_FUNC
+PyInit_acl(void) {
+    PyObject* mod = PyModule_Create(&acl);
+    if (mod == NULL) {
+        return (NULL);
+    }
+
+    try {
+        po_ACLError = PyErr_NewException("isc.acl.Error", NULL, NULL);
+        PyObjectContainer(po_ACLError).installToModule(mod, "Error");
+
+        po_LoaderError = PyErr_NewException("isc.acl.LoaderError", NULL, NULL);
+        PyObjectContainer(po_LoaderError).installToModule(mod, "LoaderError");
+
+        // Install module constants.  Note that we can let Py_BuildValue
+        // "steal" the references to these object (by specifying false to
+        // installToModule), because, unlike the exception cases above,
+        // we don't have corresponding C++ variables (see the note in
+        // pycppwrapper_util for more details).
+        PyObjectContainer(Py_BuildValue("I", isc::acl::ACCEPT)).
+            installToModule(mod, "ACCEPT", false);
+        PyObjectContainer(Py_BuildValue("I", isc::acl::REJECT)).
+            installToModule(mod, "REJECT", false);
+        PyObjectContainer(Py_BuildValue("I", isc::acl::DROP)).
+            installToModule(mod, "DROP", false);
+    } catch (...) {
+        Py_DECREF(mod);
+        return (NULL);
+    }
+
+    return (mod);
+}

+ 29 - 0
src/lib/python/isc/acl/acl.py

@@ -0,0 +1,29 @@
+# Copyright (C) 2011  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+# This file is not installed; The .so version will be installed into the right
+# place at installation time.
+# This helper script is only to find it in the .libs directory when we run
+# as a test or from the build directory.
+
+import os
+import sys
+
+for base in sys.path[:]:
+    bindingdir = os.path.join(base, 'isc/acl/.libs')
+    if os.path.exists(bindingdir):
+        sys.path.insert(0, bindingdir)
+
+from acl import *

+ 16 - 0
src/lib/python/isc/acl/acl_inc.cc

@@ -0,0 +1,16 @@
+namespace {
+const char* const acl_doc = "\
+Implementation module for ACL operations\n\n\
+This module provides Python bindings for the C++ classes in the\n\
+isc::acl namespace.\n\
+\n\
+Integer constants:\n\
+\n\
+ACCEPT, REJECT, DROP -- Default actions an ACL could perform.\n\
+  These are the commonly used actions in specific ACLs.\n\
+  It is possible to specify any other values, as the ACL class does\n\
+  nothing about them, but these look reasonable, so they are provided\n\
+  for convenience. It is not specified what exactly these mean and it's\n\
+  up to whoever uses them.\n\
+";
+} // unnamed namespace

+ 135 - 0
src/lib/python/isc/acl/dns.cc

@@ -0,0 +1,135 @@
+// Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <Python.h>
+
+#include <stdexcept>
+#include <boost/shared_ptr.hpp>
+
+#include <util/python/pycppwrapper_util.h>
+
+#include <cc/data.h>
+
+#include <acl/acl.h>
+#include <acl/dns.h>
+
+#include "dns.h"
+#include "dns_requestcontext_python.h"
+#include "dns_requestacl_python.h"
+#include "dns_requestloader_python.h"
+
+using namespace std;
+using boost::shared_ptr;
+using namespace isc::util::python;
+using namespace isc::data;
+using namespace isc::acl::dns;
+using namespace isc::acl::dns::python;
+
+#include "dnsacl_inc.cc"
+
+namespace {
+// This is a Python binding object corresponding to the singleton loader used
+// in the C++ version of the library.
+// We can define it as a pure object rather than through an accessor function,
+// because in Python we can ensure it has been created and initialized
+// in the module initializer by the time it's actually used.
+s_RequestLoader* po_REQUEST_LOADER;
+
+PyMethodDef methods[] = {
+    { NULL, NULL, 0, NULL }
+};
+
+PyModuleDef dnsacl = {
+    { PyObject_HEAD_INIT(NULL) NULL, 0, NULL},
+    "isc.acl.dns",
+    dnsacl_doc,
+    -1,
+    methods,
+    NULL,
+    NULL,
+    NULL,
+    NULL
+};
+} // end of unnamed namespace
+
+namespace isc {
+namespace acl {
+namespace dns {
+namespace python {
+PyObject*
+getACLException(const char* ex_name) {
+    PyObject* ex_obj = NULL;
+
+    PyObject* acl_module = PyImport_AddModule("isc.acl.acl");
+    if (acl_module != NULL) {
+        PyObject* acl_dict = PyModule_GetDict(acl_module);
+        if (acl_dict != NULL) {
+            ex_obj = PyDict_GetItemString(acl_dict, ex_name);
+        }
+    }
+
+    if (ex_obj == NULL) {
+        ex_obj = PyExc_RuntimeError;
+    }
+    return (ex_obj);
+}
+}
+}
+}
+}
+
+PyMODINIT_FUNC
+PyInit_dns(void) {
+    PyObject* mod = PyModule_Create(&dnsacl);
+    if (mod == NULL) {
+        return (NULL);
+    }
+
+    if (!initModulePart_RequestContext(mod)) {
+        Py_DECREF(mod);
+        return (NULL);
+    }
+    if (!initModulePart_RequestACL(mod)) {
+        Py_DECREF(mod);
+        return (NULL);
+    }
+    if (!initModulePart_RequestLoader(mod)) {
+        Py_DECREF(mod);
+        return (NULL);
+    }
+
+    // Module constants
+    try {
+        if (po_REQUEST_LOADER == NULL) {
+            po_REQUEST_LOADER = static_cast<s_RequestLoader*>(
+                requestloader_type.tp_alloc(&requestloader_type, 0));
+        }
+        if (po_REQUEST_LOADER != NULL) {
+            // We gain and keep our own reference to the singleton object
+            // for the same reason as that for exception objects (see comments
+            // in pycppwrapper_util for more details).  Note also that we don't
+            // bother to release the reference even if exception is thrown
+            // below (in fact, we cannot delete the singleton loader).
+            po_REQUEST_LOADER->cppobj = &getRequestLoader();
+            Py_INCREF(po_REQUEST_LOADER);
+        }
+        PyObjectContainer(po_REQUEST_LOADER).installToModule(mod,
+                                                             "REQUEST_LOADER");
+    } catch (...) {
+        Py_DECREF(mod);
+        return (NULL);
+    }
+
+    return (mod);
+}

+ 52 - 0
src/lib/python/isc/acl/dns.h

@@ -0,0 +1,52 @@
+// Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef __PYTHON_ACL_DNS_H
+#define __PYTHON_ACL_DNS_H 1
+
+#include <Python.h>
+
+namespace isc {
+namespace acl {
+namespace dns {
+namespace python {
+
+// Return a Python exception object of the given name (ex_name) defined in
+// the isc.acl.acl loadable module.
+//
+// Since the acl module is a different binary image and is loaded separately
+// from the dns module, it would be very tricky to directly access to
+// C/C++ symbols defined in that module.  So we get access to these object
+// using the Python interpretor through this wrapper function.
+//
+// The __init__.py file should ensure isc.acl.acl has been loaded by the time
+// whenever this function is called, and there shouldn't be any operation
+// within this function that can fail (such as dynamic memory allocation),
+// so this function should always succeed.  Yet there may be an overlooked
+// failure mode, perhaps due to a bug in the binding implementation, or
+// due to invalid usage.  As a last resort for such cases, this function
+// returns PyExc_RuntimeError (a C binding of Python's RuntimeError) should
+// it encounters an unexpected failure.
+extern PyObject* getACLException(const char* ex_name);
+
+} // namespace python
+} // namespace dns
+} // namespace acl
+} // namespace isc
+
+#endif // __PYTHON_ACL_DNS_H
+
+// Local Variables:
+// mode: c++
+// End:

+ 33 - 0
src/lib/python/isc/acl/dns.py

@@ -0,0 +1,33 @@
+# Copyright (C) 2011  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+# This file is not installed. The log.so is installed into the right place.
+# It is only to find it in the .libs directory when we run as a test or
+# from the build directory.
+# But as nobody gives us the builddir explicitly (and we can't use generation
+# from .in file, as it would put us into the builddir and we wouldn't be found)
+# we guess from current directory. Any idea for something better? This should
+# be enough for the tests, but would it work for B10_FROM_SOURCE as well?
+# Should we look there? Or define something in bind10_config?
+
+import os
+import sys
+
+for base in sys.path[:]:
+    bindingdir = os.path.join(base, 'isc/acl/.libs')
+    if os.path.exists(bindingdir):
+        sys.path.insert(0, bindingdir)
+
+from dns import *

+ 33 - 0
src/lib/python/isc/acl/dns_requestacl_inc.cc

@@ -0,0 +1,33 @@
+namespace {
+const char* const RequestACL_doc = "\
+The DNS Request ACL.\n\
+\n\
+It holds bunch of ordered entries, each one consisting of a check for\n\
+a given DNS Request context and an action, which is one of ACCEPT,\n\
+REJECT, or DROP, as defined in the isc.acl.acl module.\n\
+The checks are tested in the order and first match counts.\n\
+\n\
+A RequestACL object cannot be constructed directly; an application\n\
+must use isc.acl.dns.load_request_acl() to create a RequestACL object.\n\
+\n\
+";
+
+const char* const RequestACL_execute_doc = "\
+execute(context) -> action \n\
+\n\
+The returned action is one of ACCEPT, REJECT or DROP as defined in\n\
+the isc.acl.acl module.\n\
+\n\
+This is the function that takes the ACL entries one by one, checks the\n\
+context against conditions and if it matches, returns the action that\n\
+belongs to the first matched entry or default action if nothing\n\
+matches.\n\
+\n\
+Parameters:\n\
+  context    The thing that should be checked. It is directly passed\n\
+             to the checks.\n\
+\n\
+Return Value(s): The action for the ACL entry that first matches the\n\
+context.\n\
+";
+} // unnamed namespace

+ 184 - 0
src/lib/python/isc/acl/dns_requestacl_python.cc

@@ -0,0 +1,184 @@
+// Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+// Enable this if you use s# variants with PyArg_ParseTuple(), see
+// http://docs.python.org/py3k/c-api/arg.html#strings-and-buffers
+//#define PY_SSIZE_T_CLEAN
+
+// Python.h needs to be placed at the head of the program file, see:
+// http://docs.python.org/py3k/extending/extending.html#a-simple-example
+#include <Python.h>
+
+#include <string>
+#include <stdexcept>
+
+#include <util/python/pycppwrapper_util.h>
+
+#include <acl/acl.h>
+#include <acl/dns.h>
+
+#include "dns.h"
+#include "dns_requestacl_python.h"
+#include "dns_requestcontext_python.h"
+
+using namespace std;
+using namespace isc::util::python;
+using namespace isc::acl;
+using namespace isc::acl::dns;
+using namespace isc::acl::dns::python;
+
+//
+// Definition of the classes
+//
+
+// For each class, we need a struct, a helper functions (init, destroy,
+// and static wrappers around the methods we export), a list of methods,
+// and a type description
+
+//
+// RequestACL
+//
+
+// Trivial constructor.
+s_RequestACL::s_RequestACL() {}
+
+// Import pydoc text
+#include "dns_requestacl_inc.cc"
+
+namespace {
+int
+RequestACL_init(PyObject*, PyObject*, PyObject*) {
+    PyErr_SetString(getACLException("Error"),
+                    "RequestACL cannot be directly constructed");
+    return (-1);
+}
+
+void
+RequestACL_destroy(PyObject* po_self) {
+    s_RequestACL* const self = static_cast<s_RequestACL*>(po_self);
+    self->cppobj.reset();
+    Py_TYPE(self)->tp_free(self);
+}
+
+PyObject*
+RequestACL_execute(PyObject* po_self, PyObject* args) {
+    s_RequestACL* const self = static_cast<s_RequestACL*>(po_self);
+
+    try {
+        const s_RequestContext* po_context;
+        if (PyArg_ParseTuple(args, "O!", &requestcontext_type, &po_context)) {
+            const BasicAction action =
+                self->cppobj->execute(*po_context->cppobj);
+            return (Py_BuildValue("I", action));
+        }
+    } catch (const exception& ex) {
+        const string ex_what = "Failed to execute ACL: " + string(ex.what());
+        PyErr_SetString(getACLException("Error"), ex_what.c_str());
+    } catch (...) {
+        PyErr_SetString(PyExc_RuntimeError,
+                        "Unexpected exception in executing ACL");
+    }
+
+    return (NULL);
+}
+
+// This list contains the actual set of functions we have in
+// python. Each entry has
+// 1. Python method name
+// 2. Our static function here
+// 3. Argument type
+// 4. Documentation
+PyMethodDef RequestACL_methods[] = {
+    { "execute", RequestACL_execute, METH_VARARGS, RequestACL_execute_doc },
+    { NULL, NULL, 0, NULL }
+};
+} // end of unnamed namespace
+
+namespace isc {
+namespace acl {
+namespace dns {
+namespace python {
+// This defines the complete type for reflection in python and
+// parsing of PyObject* to s_RequestACL
+// Most of the functions are not actually implemented and NULL here.
+PyTypeObject requestacl_type = {
+    PyVarObject_HEAD_INIT(NULL, 0)
+    "isc.acl.dns.RequestACL",
+    sizeof(s_RequestACL),                 // tp_basicsize
+    0,                                  // tp_itemsize
+    RequestACL_destroy,                // tp_dealloc
+    NULL,                               // tp_print
+    NULL,                               // tp_getattr
+    NULL,                               // tp_setattr
+    NULL,                               // tp_reserved
+    NULL,                               // tp_repr
+    NULL,                               // tp_as_number
+    NULL,                               // tp_as_sequence
+    NULL,                               // tp_as_mapping
+    NULL,                               // tp_hash
+    NULL,                               // tp_call
+    NULL,	                       // tp_str
+    NULL,                               // tp_getattro
+    NULL,                               // tp_setattro
+    NULL,                               // tp_as_buffer
+    Py_TPFLAGS_DEFAULT,                 // tp_flags
+    RequestACL_doc,
+    NULL,                               // tp_traverse
+    NULL,                               // tp_clear
+    NULL,				 // tp_richcompare
+    0,                                  // tp_weaklistoffset
+    NULL,                               // tp_iter
+    NULL,                               // tp_iternext
+    RequestACL_methods,                   // tp_methods
+    NULL,                               // tp_members
+    NULL,                               // tp_getset
+    NULL,                               // tp_base
+    NULL,                               // tp_dict
+    NULL,                               // tp_descr_get
+    NULL,                               // tp_descr_set
+    0,                                  // tp_dictoffset
+    RequestACL_init,                    // tp_init
+    NULL,                               // tp_alloc
+    PyType_GenericNew,                  // tp_new
+    NULL,                               // tp_free
+    NULL,                               // tp_is_gc
+    NULL,                               // tp_bases
+    NULL,                               // tp_mro
+    NULL,                               // tp_cache
+    NULL,                               // tp_subclasses
+    NULL,                               // tp_weaklist
+    NULL,                               // tp_del
+    0                                   // tp_version_tag
+};
+
+bool
+initModulePart_RequestACL(PyObject* mod) {
+    // We initialize the static description object with PyType_Ready(),
+    // then add it to the module. This is not just a check! (leaving
+    // this out results in segmentation faults)
+    if (PyType_Ready(&requestacl_type) < 0) {
+        return (false);
+    }
+    void* p = &requestacl_type;
+    if (PyModule_AddObject(mod, "RequestACL", static_cast<PyObject*>(p)) < 0) {
+        return (false);
+    }
+    Py_INCREF(&requestacl_type);
+
+    return (true);
+}
+} // namespace python
+} // namespace dns
+} // namespace acl
+} // namespace isc

+ 53 - 0
src/lib/python/isc/acl/dns_requestacl_python.h

@@ -0,0 +1,53 @@
+// Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef __PYTHON_REQUESTACL_H
+#define __PYTHON_REQUESTACL_H 1
+
+#include <Python.h>
+
+#include <boost/shared_ptr.hpp>
+
+#include <acl/dns.h>
+
+namespace isc {
+namespace acl {
+namespace dns {
+namespace python {
+
+// The s_* Class simply covers one instantiation of the object
+class s_RequestACL : public PyObject {
+public:
+    s_RequestACL();
+
+    // We don't have to use a shared pointer for its original purposes as
+    // the python object maintains reference counters itself.  But the
+    // underlying C++ API only exposes a shared pointer for the ACL objects,
+    // so we store it in that form.
+    boost::shared_ptr<RequestACL> cppobj;
+};
+
+extern PyTypeObject requestacl_type;
+
+bool initModulePart_RequestACL(PyObject* mod);
+
+} // namespace python
+} // namespace dns
+} // namespace acl
+} // namespace isc
+#endif // __PYTHON_REQUESTACL_H
+
+// Local Variables:
+// mode: c++
+// End:

+ 30 - 0
src/lib/python/isc/acl/dns_requestcontext_inc.cc

@@ -0,0 +1,30 @@
+namespace {
+const char* const RequestContext_doc = "\
+DNS request to be checked.\n\
+\n\
+This plays the role of ACL context for the RequestACL object.\n\
+\n\
+Based on the minimalist philosophy, the initial implementation only\n\
+maintains the remote (source) IP address of the request. The plan is\n\
+to add more parameters of the request. A scheduled next step is to\n\
+support the TSIG key (if it's included in the request). Other\n\
+possibilities are the local (destination) IP address, the remote and\n\
+local port numbers, various fields of the DNS request (e.g. a\n\
+particular header flag value).\n\
+\n\
+RequestContext(remote_address)\n\
+\n\
+    In this initial implementation, the constructor only takes a\n\
+    remote IP address in the form of a socket address as used in the\n\
+    Python socket module.\n\
+\n\
+    Exceptions:\n\
+      isc.acl.ACLError Normally shouldn't happen, but still possible\n\
+                     for unexpected errors such as memory allocation\n\
+                     failure or an invalid address text being passed.\n\
+\n\
+    Parameters:\n\
+      remote_address The remote IP address\n\
+\n\
+";
+} // unnamed namespace

+ 319 - 0
src/lib/python/isc/acl/dns_requestcontext_python.cc

@@ -0,0 +1,319 @@
+// Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+// Enable this if you use s# variants with PyArg_ParseTuple(), see
+// http://docs.python.org/py3k/c-api/arg.html#strings-and-buffers
+//#define PY_SSIZE_T_CLEAN
+
+// Python.h needs to be placed at the head of the program file, see:
+// http://docs.python.org/py3k/extending/extending.html#a-simple-example
+#include <Python.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netdb.h>
+#include <string.h>
+
+#include <cassert>
+#include <memory>
+#include <string>
+#include <sstream>
+#include <stdexcept>
+
+#include <boost/scoped_ptr.hpp>
+#include <boost/lexical_cast.hpp>
+
+#include <exceptions/exceptions.h>
+
+#include <util/python/pycppwrapper_util.h>
+
+#include <acl/dns.h>
+#include <acl/ip_check.h>
+
+#include "dns.h"
+#include "dns_requestcontext_python.h"
+
+using namespace std;
+using boost::scoped_ptr;
+using boost::lexical_cast;
+using namespace isc;
+using namespace isc::util::python;
+using namespace isc::acl::dns;
+using namespace isc::acl::dns::python;
+
+namespace isc {
+namespace acl {
+namespace dns {
+namespace python {
+
+struct s_RequestContext::Data {
+    // The constructor.  Currently it only accepts the information of the
+    // request source address, and contains all necessary logic in the body
+    // of the constructor.  As it's extended we may have refactor it by
+    // introducing helper methods.
+    Data(const char* const remote_addr, const unsigned short remote_port) {
+        struct addrinfo hints, *res;
+        memset(&hints, 0, sizeof(hints));
+        hints.ai_family = AF_UNSPEC;
+        hints.ai_socktype = SOCK_DGRAM;
+        hints.ai_protocol = IPPROTO_UDP;
+        hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV;
+        const int error(getaddrinfo(remote_addr,
+                                    lexical_cast<string>(remote_port).c_str(),
+                                    &hints, &res));
+        if (error != 0) {
+            isc_throw(InvalidParameter, "Failed to convert [" << remote_addr
+                      << "]:" << remote_port << ", " << gai_strerror(error));
+        }
+        assert(sizeof(remote_ss) > res->ai_addrlen);
+        memcpy(&remote_ss, res->ai_addr, res->ai_addrlen);
+        remote_salen = res->ai_addrlen;
+        freeaddrinfo(res);
+
+        remote_ipaddr.reset(new IPAddress(getRemoteSockaddr()));
+    }
+
+    // A convenient type converter from sockaddr_storage to sockaddr
+    const struct sockaddr& getRemoteSockaddr() const {
+        const void* p = &remote_ss;
+        return (*static_cast<const struct sockaddr*>(p));
+    }
+
+    // The remote (source) IP address the request.  Note that it needs
+    // a reference to remote_ss.  That's why the latter is stored within
+    // this structure.
+    scoped_ptr<IPAddress> remote_ipaddr;
+
+    // The effective length of remote_ss.  It's necessary for getnameinf()
+    // called from sockaddrToText (__str__ backend).
+    socklen_t remote_salen;
+
+private:
+    struct sockaddr_storage remote_ss;
+};
+
+} // namespace python
+} // namespace dns
+} // namespace acl
+} // namespace isc
+
+
+//
+// Definition of the classes
+//
+
+// For each class, we need a struct, a helper functions (init, destroy,
+// and static wrappers around the methods we export), a list of methods,
+// and a type description
+
+//
+// RequestContext
+//
+
+// Trivial constructor.
+s_RequestContext::s_RequestContext() : cppobj(NULL), data_(NULL) {
+}
+
+// Import pydoc text
+#include "dns_requestcontext_inc.cc"
+
+namespace {
+// This list contains the actual set of functions we have in
+// python. Each entry has
+// 1. Python method name
+// 2. Our static function here
+// 3. Argument type
+// 4. Documentation
+PyMethodDef RequestContext_methods[] = {
+    { NULL, NULL, 0, NULL }
+};
+
+int
+RequestContext_init(PyObject* po_self, PyObject* args, PyObject*) {
+    s_RequestContext* const self = static_cast<s_RequestContext*>(po_self);
+
+    try {
+        // In this initial implementation, the constructor is simply: It
+        // takes a single parameter, which should be a Python socket address
+        // object.  For IPv4, it's ('address test', numeric_port); for IPv6,
+        // it's ('address text', num_port, num_flowid, num_zoneid).
+        // Below, we parse the argument in the most straightforward way.
+        // As the constructor becomes more complicated, we should probably
+        // make it more structural (for example, we should first retrieve
+        // the socket address as a PyObject, and parse it recursively)
+
+        const char* remote_addr;
+        unsigned short remote_port;
+        unsigned int remote_flowinfo; // IPv6 only, unused here
+        unsigned int remote_zoneid; // IPv6 only, unused here
+
+        if (PyArg_ParseTuple(args, "(sH)", &remote_addr, &remote_port) ||
+            PyArg_ParseTuple(args, "(sHII)", &remote_addr, &remote_port,
+                             &remote_flowinfo, &remote_zoneid))
+        {
+            // We need to clear the error in case the first call to PareTuple
+            // fails.
+            PyErr_Clear();
+
+            auto_ptr<s_RequestContext::Data> dataptr(
+                new s_RequestContext::Data(remote_addr, remote_port));
+            self->cppobj = new RequestContext(*dataptr->remote_ipaddr);
+            self->data_ = dataptr.release();
+            return (0);
+        }
+    } catch (const exception& ex) {
+        const string ex_what = "Failed to construct RequestContext object: " +
+            string(ex.what());
+        PyErr_SetString(getACLException("Error"), ex_what.c_str());
+        return (-1);
+    } catch (...) {
+        PyErr_SetString(PyExc_RuntimeError,
+                        "Unexpected exception in constructing RequestContext");
+        return (-1);
+    }
+
+    PyErr_SetString(PyExc_TypeError,
+                    "Invalid arguments to RequestContext constructor");
+
+    return (-1);
+}
+
+void
+RequestContext_destroy(PyObject* po_self) {
+    s_RequestContext* const self = static_cast<s_RequestContext*>(po_self);
+
+    delete self->cppobj;
+    delete self->data_;
+    Py_TYPE(self)->tp_free(self);
+}
+
+// A helper function for __str__()
+string
+sockaddrToText(const struct sockaddr& sa, socklen_t sa_len) {
+    char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
+    if (getnameinfo(&sa, sa_len, hbuf, sizeof(hbuf), sbuf, sizeof(sbuf),
+                    NI_NUMERICHOST | NI_NUMERICSERV)) {
+        // In this context this should never fail.
+        isc_throw(Unexpected, "Unexpected failure in getnameinfo");
+    }
+
+    return ("[" + string(hbuf) + "]:" + string(sbuf));
+}
+
+// for the __str__() method.  This method is provided mainly for internal
+// testing.
+PyObject*
+RequestContext_str(PyObject* po_self) {
+    const s_RequestContext* const self =
+        static_cast<s_RequestContext*>(po_self);
+
+    try {
+        stringstream objss;
+        objss << "<" << requestcontext_type.tp_name << " object, "
+              << "remote_addr="
+              << sockaddrToText(self->data_->getRemoteSockaddr(),
+                                self->data_->remote_salen) << ">";
+        return (Py_BuildValue("s", objss.str().c_str()));
+    } catch (const exception& ex) {
+        const string ex_what =
+            "Failed to convert RequestContext object to text: " +
+            string(ex.what());
+        PyErr_SetString(PyExc_RuntimeError, ex_what.c_str());
+    } catch (...) {
+        PyErr_SetString(PyExc_SystemError, "Unexpected failure in "
+                        "converting RequestContext object to text");
+    }
+    return (NULL);
+}
+} // end of unnamed namespace
+
+namespace isc {
+namespace acl {
+namespace dns {
+namespace python {
+// This defines the complete type for reflection in python and
+// parsing of PyObject* to s_RequestContext
+// Most of the functions are not actually implemented and NULL here.
+PyTypeObject requestcontext_type = {
+    PyVarObject_HEAD_INIT(NULL, 0)
+    "isc.acl.dns.RequestContext",
+    sizeof(s_RequestContext),                 // tp_basicsize
+    0,                                  // tp_itemsize
+    RequestContext_destroy,             // tp_dealloc
+    NULL,                               // tp_print
+    NULL,                               // tp_getattr
+    NULL,                               // tp_setattr
+    NULL,                               // tp_reserved
+    NULL,                               // tp_repr
+    NULL,                               // tp_as_number
+    NULL,                               // tp_as_sequence
+    NULL,                               // tp_as_mapping
+    NULL,                               // tp_hash
+    NULL,                               // tp_call
+    RequestContext_str,                 // tp_str
+    NULL,                               // tp_getattro
+    NULL,                               // tp_setattro
+    NULL,                               // tp_as_buffer
+    Py_TPFLAGS_DEFAULT,                 // tp_flags
+    RequestContext_doc,
+    NULL,                               // tp_traverse
+    NULL,                               // tp_clear
+    NULL, // tp_richcompare
+    0,                                  // tp_weaklistoffset
+    NULL,                               // tp_iter
+    NULL,                               // tp_iternext
+    RequestContext_methods,                   // tp_methods
+    NULL,                               // tp_members
+    NULL,                               // tp_getset
+    NULL,                               // tp_base
+    NULL,                               // tp_dict
+    NULL,                               // tp_descr_get
+    NULL,                               // tp_descr_set
+    0,                                  // tp_dictoffset
+    RequestContext_init,                // tp_init
+    NULL,                               // tp_alloc
+    PyType_GenericNew,                  // tp_new
+    NULL,                               // tp_free
+    NULL,                               // tp_is_gc
+    NULL,                               // tp_bases
+    NULL,                               // tp_mro
+    NULL,                               // tp_cache
+    NULL,                               // tp_subclasses
+    NULL,                               // tp_weaklist
+    NULL,                               // tp_del
+    0                                   // tp_version_tag
+};
+
+bool
+initModulePart_RequestContext(PyObject* mod) {
+    // We initialize the static description object with PyType_Ready(),
+    // then add it to the module. This is not just a check! (leaving
+    // this out results in segmentation faults)
+    if (PyType_Ready(&requestcontext_type) < 0) {
+        return (false);
+    }
+    void* p = &requestcontext_type;
+    if (PyModule_AddObject(mod, "RequestContext",
+                           static_cast<PyObject*>(p)) < 0) {
+        return (false);
+    }
+    Py_INCREF(&requestcontext_type);
+
+    return (true);
+}
+} // namespace python
+} // namespace dns
+} // namespace acl
+} // namespace isc

+ 54 - 0
src/lib/python/isc/acl/dns_requestcontext_python.h

@@ -0,0 +1,54 @@
+// Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef __PYTHON_REQUESTCONTEXT_H
+#define __PYTHON_REQUESTCONTEXT_H 1
+
+#include <Python.h>
+
+#include <acl/dns.h>
+
+namespace isc {
+namespace acl {
+namespace dns {
+namespace python {
+
+// The s_* Class simply covers one instantiation of the object
+class s_RequestContext : public PyObject {
+public:
+    s_RequestContext();
+    RequestContext* cppobj;
+
+    // This object needs to maintain some source data to construct the
+    // underlying RequestContext object throughout its lifetime.
+    // These are "public" so that it can be accessed in the python wrapper
+    // implementation, but essentially they should be private, and the
+    // implementation details are hidden.
+    struct Data;
+    Data* data_;
+};
+
+extern PyTypeObject requestcontext_type;
+
+bool initModulePart_RequestContext(PyObject* mod);
+
+} // namespace python
+} // namespace dns
+} // namespace acl
+} // namespace isc
+#endif // __PYTHON_REQUESTCONTEXT_H
+
+// Local Variables:
+// mode: c++
+// End:

+ 87 - 0
src/lib/python/isc/acl/dns_requestloader_inc.cc

@@ -0,0 +1,87 @@
+namespace {
+
+// Note: this is derived from the generic Loader class of the C++
+// implementation, but is slightly different from the original.
+// Be careful when you make further merge from the C++ document.
+const char* const RequestLoader_doc = "\
+Loader of DNS Request ACLs.\n\
+\n\
+The goal of this class is to convert JSON description of an ACL to\n\
+object of the ACL class (including the checks inside it).\n\
+\n\
+To allow any kind of checks to exist in the application, creators are\n\
+registered for the names of the checks (this feature is not yet\n\
+available for the python API).\n\
+\n\
+An ACL definition looks like this:  [\n\
+   {\n\
+      \"action\": \"ACCEPT\",\n\
+      \"match-type\": <parameter>\n\
+   },\n\
+   {\n\
+      \"action\": \"REJECT\",\n\
+      \"match-type\": <parameter>,\n\
+      \"another-match-type\": [<parameter1>, <parameter2>]\n\
+   },\n\
+   {\n\
+      \"action\": \"DROP\"\n\
+   }\n\
+ ]\n\
+ \n\
+\n\
+This is a list of elements. Each element must have an \"action\"\n\
+entry/keyword. That one specifies which action is returned if this\n\
+element matches (the value of the key is passed to the action loader\n\
+(see the constructor), which is one of ACCEPT,\n\
+REJECT, or DROP, as defined in the isc.acl.acl module.\n\
+\n\
+The rest of the element are matches. The left side is the name of the\n\
+match type (for example \"from\" to match for source IP address).\n\
+The <parameter> is whatever is needed to describe the\n\
+match and depends on the match type, the loader passes it verbatim to\n\
+creator of that match type.\n\
+\n\
+There may be multiple match types in single element. In such case, all\n\
+of the matches must match for the element to take action (so, in the\n\
+second element, both \"match-type\" and \"another-match-type\" must be\n\
+satisfied). If there's no match in the element, the action is\n\
+taken/returned without conditions, every time (makes sense as the last\n\
+entry, as the ACL will never get past it).\n\
+\n\
+The second entry shows another thing - if there's a list as the value\n\
+for some match and the match itself is not expecting a list, it is\n\
+taken as an \"or\" - a match for at last one of the choices in the\n\
+list must match. So, for the second entry, both \"match-type\" and\n\
+\"another-match-type\" must be satisfied, but the another one is\n\
+satisfied by either parameter1 or parameter2.\n\
+\n\
+Currently, a RequestLoader object cannot be constructed directly;\n\
+an application must use the singleton loader defined in the\n\
+isc.acl.dns module, i.e., isc.acl.dns.REQUEST_LOADER.\n\
+A future version of this implementation may be extended to give\n\
+applications full flexibility of creating arbitrary loader, when\n\
+this restriction may be removed.\n\
+";
+
+const char* const RequestLoader_load_doc = "\
+load(description) -> RequestACL\n\
+\n\
+Load a DNS (Request) ACL.\n\
+\n\
+This parses an ACL list, creates internal data for each rule\n\
+and returns a RequestACl object that contains all given rules.\n\
+\n\
+Exceptions:\n\
+  LoaderError Load failed.  The most likely cause of this is a syntax\n\
+              error in the description.  Other internal errors such as\n\
+              memory allocation failure is also converted to this\n\
+              exception.\n\
+\n\
+Parameters:\n\
+  description String or Python representation of the JSON list of\n\
+              ACL. The Python representation is ones accepted by the\n\
+              standard json module.\n\
+\n\
+Return Value(s): The newly created RequestACL object\n\
+";
+} // unnamed namespace

+ 270 - 0
src/lib/python/isc/acl/dns_requestloader_python.cc

@@ -0,0 +1,270 @@
+// Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+// Enable this if you use s# variants with PyArg_ParseTuple(), see
+// http://docs.python.org/py3k/c-api/arg.html#strings-and-buffers
+//#define PY_SSIZE_T_CLEAN
+
+// Python.h needs to be placed at the head of the program file, see:
+// http://docs.python.org/py3k/extending/extending.html#a-simple-example
+#include <Python.h>
+
+#include <string>
+#include <stdexcept>
+
+#include <boost/shared_ptr.hpp>
+
+#include <util/python/pycppwrapper_util.h>
+
+#include <cc/data.h>
+
+#include <acl/dns.h>
+
+#include "dns.h"
+#include "dns_requestacl_python.h"
+#include "dns_requestloader_python.h"
+
+using namespace std;
+using boost::shared_ptr;
+using namespace isc::util::python;
+using namespace isc::data;
+using namespace isc::acl::dns;
+using namespace isc::acl::dns::python;
+
+//
+// Definition of the classes
+//
+
+// For each class, we need a struct, a helper functions (init, destroy,
+// and static wrappers around the methods we export), a list of methods,
+// and a type description
+
+//
+// RequestLoader
+//
+
+// Trivial constructor.
+s_RequestLoader::s_RequestLoader() : cppobj(NULL) {
+}
+
+// Import pydoc text
+#include "dns_requestloader_inc.cc"
+
+namespace {
+//
+// We declare the functions here, the definitions are below
+// the type definition of the object, since both can use the other
+//
+
+int
+RequestLoader_init(PyObject*, PyObject*, PyObject*) {
+    PyErr_SetString(getACLException("Error"),
+                    "RequestLoader cannot be directly constructed");
+    return (-1);
+}
+
+void
+RequestLoader_destroy(PyObject* po_self) {
+    s_RequestLoader* const self = static_cast<s_RequestLoader*>(po_self);
+    delete self->cppobj;
+    self->cppobj = NULL;
+    Py_TYPE(self)->tp_free(self);
+}
+
+// This C structure corresponds to a Python callable object for json.dumps().
+// This is initialized at the class initialization time (in
+// initModulePart_RequestLoader() below) and it's ensured to be non NULL and
+// valid in the rest of the class implementation.
+// Getting access to the json module this way and call one of its functions
+// via PyObject_CallObject() may exceed the reasonably acceptable level for
+// straightforward bindings.  But the alternative would be to write a Python
+// frontend for the entire module only for this conversion, which would also
+// be too much.  So, right now, we implement everything within the binding
+// implementation.  If future extensions require more such non trivial
+// wrappers, we should consider the frontend approach more seriously.
+PyObject* json_dumps_obj = NULL;
+
+PyObject*
+RequestLoader_load(PyObject* po_self, PyObject* args) {
+    s_RequestLoader* const self = static_cast<s_RequestLoader*>(po_self);
+
+    try {
+        PyObjectContainer c1, c2; // placeholder for temporary py objects
+        const char* acl_config;
+
+        // First, try string
+        int py_result = PyArg_ParseTuple(args, "s", &acl_config);
+        if (!py_result) {
+            PyErr_Clear();  // need to clear the error from ParseTuple
+
+            // If that fails, confirm the argument is a single Python object,
+            // and pass the argument to json.dumps() without conversion.
+            // Note that we should pass 'args', not 'json_obj' to
+            // PyObject_CallObject(), since this function expects a form of
+            // tuple as its argument parameter, just like ParseTuple.
+            PyObject* json_obj;
+            if (PyArg_ParseTuple(args, "O", &json_obj)) {
+                c1.reset(PyObject_CallObject(json_dumps_obj, args));
+                c2.reset(Py_BuildValue("(O)", c1.get()));
+                py_result = PyArg_ParseTuple(c2.get(), "s", &acl_config);
+            }
+        }
+        if (py_result) {
+            shared_ptr<RequestACL> acl(
+                self->cppobj->load(Element::fromJSON(acl_config)));
+            s_RequestACL* py_acl = static_cast<s_RequestACL*>(
+                requestacl_type.tp_alloc(&requestacl_type, 0));
+            if (py_acl != NULL) {
+                py_acl->cppobj = acl;
+            }
+            return (py_acl);
+        }
+    } catch (const PyCPPWrapperException&) {
+        // If the wrapper utility throws, it's most likely because an invalid
+        // type of argument is passed (and the call to json.dumps() failed
+        // above), rather than a rare case of system errors such as memory
+        // allocation failure.  So we fall through to the end of this function
+        // and raise a TypeError.
+        ;
+    } catch (const exception& ex) {
+        PyErr_SetString(getACLException("LoaderError"), ex.what());
+        return (NULL);
+    } catch (...) {
+        PyErr_SetString(PyExc_SystemError, "Unexpected C++ exception");
+        return (NULL);
+    }
+
+    PyErr_SetString(PyExc_TypeError, "RequestLoader.load() "
+                    "expects str or python representation of JSON");
+    return (NULL);
+}
+
+// This list contains the actual set of functions we have in
+// python. Each entry has
+// 1. Python method name
+// 2. Our static function here
+// 3. Argument type
+// 4. Documentation
+PyMethodDef RequestLoader_methods[] = {
+    { "load", RequestLoader_load, METH_VARARGS, RequestLoader_load_doc },
+    { NULL, NULL, 0, NULL }
+};
+} // end of unnamed namespace
+
+namespace isc {
+namespace acl {
+namespace dns {
+namespace python {
+// This defines the complete type for reflection in python and
+// parsing of PyObject* to s_RequestLoader
+// Most of the functions are not actually implemented and NULL here.
+PyTypeObject requestloader_type = {
+    PyVarObject_HEAD_INIT(NULL, 0)
+    "isc.acl.dns.RequestLoader",
+    sizeof(s_RequestLoader),                 // tp_basicsize
+    0,                                  // tp_itemsize
+    RequestLoader_destroy,       // tp_dealloc
+    NULL,                               // tp_print
+    NULL,                               // tp_getattr
+    NULL,                               // tp_setattr
+    NULL,                               // tp_reserved
+    NULL,                               // tp_repr
+    NULL,                               // tp_as_number
+    NULL,                               // tp_as_sequence
+    NULL,                               // tp_as_mapping
+    NULL,                               // tp_hash
+    NULL,                               // tp_call
+    NULL,                       // tp_str
+    NULL,                               // tp_getattro
+    NULL,                               // tp_setattro
+    NULL,                               // tp_as_buffer
+    Py_TPFLAGS_DEFAULT,                 // tp_flags
+    RequestLoader_doc,
+    NULL,                               // tp_traverse
+    NULL,                               // tp_clear
+    NULL, // tp_richcompare
+    0,                                  // tp_weaklistoffset
+    NULL,                               // tp_iter
+    NULL,                               // tp_iternext
+    RequestLoader_methods,                   // tp_methods
+    NULL,                               // tp_members
+    NULL,                               // tp_getset
+    NULL,                               // tp_base
+    NULL,                               // tp_dict
+    NULL,                               // tp_descr_get
+    NULL,                               // tp_descr_set
+    0,                                  // tp_dictoffset
+    RequestLoader_init,            // tp_init
+    NULL,                               // tp_alloc
+    PyType_GenericNew,                  // tp_new
+    NULL,                               // tp_free
+    NULL,                               // tp_is_gc
+    NULL,                               // tp_bases
+    NULL,                               // tp_mro
+    NULL,                               // tp_cache
+    NULL,                               // tp_subclasses
+    NULL,                               // tp_weaklist
+    NULL,                               // tp_del
+    0                                   // tp_version_tag
+};
+
+bool
+initModulePart_RequestLoader(PyObject* mod) {
+    // We initialize the static description object with PyType_Ready(),
+    // then add it to the module. This is not just a check! (leaving
+    // this out results in segmentation faults)
+    if (PyType_Ready(&requestloader_type) < 0) {
+        return (false);
+    }
+    void* p = &requestloader_type;
+    if (PyModule_AddObject(mod, "RequestLoader",
+                           static_cast<PyObject*>(p)) < 0) {
+        return (false);
+    }
+
+    // Get and hold our own reference to json.dumps() for later use.
+    // Normally it should succeed as __init__.py of the isc.acl package
+    // explicitly imports the json module, and the code below should be
+    // error free (e.g. they don't require memory allocation) under this
+    // condition.
+    // This could still fail with deviant or evil Python code such as those
+    // that first import json and then delete the reference to it from
+    // sys.modules before it imports the acl.dns module.  The RequestLoader
+    // class could still work as long as it doesn't use the JSON decoder,
+    // but we'd rather refuse to import the module than allowing the partially
+    // workable class to keep running.
+    PyObject* json_module = PyImport_AddModule("json");
+    if (json_module != NULL) {
+        PyObject* json_dict = PyModule_GetDict(json_module);
+        if (json_dict != NULL) {
+            json_dumps_obj = PyDict_GetItemString(json_dict, "dumps");
+        }
+    }
+    if (json_dumps_obj != NULL) {
+        Py_INCREF(json_dumps_obj);
+    } else {
+        PyErr_SetString(PyExc_RuntimeError,
+                        "isc.acl.dns.RequestLoader needs the json module, but "
+                        "it's missing");
+        return (false);
+    }
+
+    Py_INCREF(&requestloader_type);
+
+    return (true);
+}
+} // namespace python
+} // namespace dns
+} // namespace acl
+} // namespace isc

+ 46 - 0
src/lib/python/isc/acl/dns_requestloader_python.h

@@ -0,0 +1,46 @@
+// Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef __PYTHON_REQUESTLOADER_H
+#define __PYTHON_REQUESTLOADER_H 1
+
+#include <Python.h>
+
+#include <acl/dns.h>
+
+namespace isc {
+namespace acl {
+namespace dns {
+namespace python {
+
+// The s_* Class simply covers one instantiation of the object
+class s_RequestLoader : public PyObject {
+public:
+    s_RequestLoader();
+    RequestLoader* cppobj;
+};
+
+extern PyTypeObject requestloader_type;
+
+bool initModulePart_RequestLoader(PyObject* mod);
+
+} // namespace python
+} // namespace dns
+} // namespace acl
+} // namespace isc
+#endif // __PYTHON_REQUESTLOADER_H
+
+// Local Variables:
+// mode: c++
+// End:

+ 17 - 0
src/lib/python/isc/acl/dnsacl_inc.cc

@@ -0,0 +1,17 @@
+namespace {
+const char* const dnsacl_doc = "\
+Implementation module for DNS ACL operations\n\n\
+This module provides Python bindings for the C++ classes in the\n\
+isc::acl::dns namespace.  Specifically, it defines Python interfaces of\n\
+handling access control lists (ACLs) with DNS related contexts.\n\
+These bindings are close match to the C++ API, but they are not complete\n\
+(some parts are not needed) and some are done in more python-like ways.\n\
+\n\
+Special objects:\n\
+\n\
+REQUEST_LOADER -- A singleton loader of ACLs. It is expected applications\n\
+  will use this function instead of creating their own loaders, because\n\
+  one is enough, this one will have registered default checks and it is\n\
+  known one, so any plugins can registrer additional checks as well.\n\
+";
+} // unnamed namespace

+ 30 - 0
src/lib/python/isc/acl/tests/Makefile.am

@@ -0,0 +1,30 @@
+PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
+PYTESTS = acl_test.py dns_test.py
+
+EXTRA_DIST = $(PYTESTS)
+
+# If necessary (rare cases), explicitly specify paths to dynamic libraries
+# required by loadable python modules.
+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/acl/.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:$$$(ENV_LIBRARY_PATH)
+endif
+
+# test using command-line arguments, so use check-local target instead of TESTS
+check-local:
+if ENABLE_PYTHON_COVERAGE
+	touch $(abs_top_srcdir)/.coverage
+	rm -f .coverage
+	${LN_S} $(abs_top_srcdir)/.coverage .coverage
+endif
+	for pytest in $(PYTESTS) ; do \
+	echo Running test: $$pytest ; \
+	env PYTHONPATH=$(abs_top_builddir)/src/lib/isc/python/acl/.libs:$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python \
+	$(LIBRARY_PATH_PLACEHOLDER) \
+	$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
+	done
+
+CLEANDIRS = __pycache__
+
+clean-local:
+	rm -rf $(CLEANDIRS)

+ 29 - 0
src/lib/python/isc/acl/tests/acl_test.py

@@ -0,0 +1,29 @@
+# Copyright (C) 2011  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import unittest
+from isc.acl.acl import *
+
+class ACLTest(unittest.TestCase):
+
+    def test_actions(self):
+        # These are simple tests just checking the pre defined actions have
+        # different values
+        self.assertTrue(ACCEPT != REJECT)
+        self.assertTrue(REJECT != DROP)
+        self.assertTrue(DROP != ACCEPT)
+
+if __name__ == '__main__':
+    unittest.main()

+ 280 - 0
src/lib/python/isc/acl/tests/dns_test.py

@@ -0,0 +1,280 @@
+# Copyright (C) 2011  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import unittest
+import socket
+from isc.acl.acl import LoaderError, Error, ACCEPT, REJECT, DROP
+from isc.acl.dns import *
+
+def get_sockaddr(address, port):
+    '''This is a simple shortcut wrapper for getaddrinfo'''
+    ai = socket.getaddrinfo(address, port, 0, socket.SOCK_DGRAM,
+                            socket.IPPROTO_UDP, socket.AI_NUMERICHOST)[0]
+    return ai[4]
+
+def get_acl(prefix):
+    '''This is a simple shortcut for creating an ACL containing single rule
+    that accepts addresses for the given IP prefix (and reject any others
+    by default)
+    '''
+    return REQUEST_LOADER.load('[{"action": "ACCEPT", "from": "' + \
+                                   prefix + '"}]')
+
+def get_acl_json(prefix):
+    '''Same as get_acl, but this function passes a Python representation of
+    JSON to the loader, not a string.'''
+    json = [{"action": "ACCEPT"}]
+    json[0]["from"] = prefix
+    return REQUEST_LOADER.load(json)
+
+def get_context(address):
+    '''This is a simple shortcut wrapper for creating a RequestContext
+    object with a given IP address.  Port number doesn't matter in the test
+    (as of the initial implementation), so it's fixed for simplicity.
+    '''
+    return RequestContext(get_sockaddr(address, 53000))
+
+# These are commonly used RequestContext object
+CONTEXT4 = get_context('192.0.2.1')
+CONTEXT6 = get_context('2001:db8::1')
+
+class RequestContextTest(unittest.TestCase):
+
+    def test_construct(self):
+        # Construct the context from IPv4/IPv6 addresses, check the object
+        # by printing it.
+        self.assertEqual('<isc.acl.dns.RequestContext object, ' + \
+                             'remote_addr=[192.0.2.1]:53001>',
+                         RequestContext(('192.0.2.1', 53001)).__str__())
+        self.assertEqual('<isc.acl.dns.RequestContext object, ' + \
+                             'remote_addr=[2001:db8::1234]:53006>',
+                         RequestContext(('2001:db8::1234', 53006,
+                                         0, 0)).__str__())
+
+        # Unusual case: port number overflows (this constructor allows that,
+        # although it should be rare anyway; the socket address should
+        # normally come from the Python socket module.
+        self.assertEqual('<isc.acl.dns.RequestContext object, ' + \
+                             'remote_addr=[192.0.2.1]:0>',
+                         RequestContext(('192.0.2.1', 65536)).__str__())
+
+        # same test using socket.getaddrinfo() to ensure it accepts the sock
+        # address representation used in the Python socket module.
+        self.assertEqual('<isc.acl.dns.RequestContext object, ' + \
+                             'remote_addr=[192.0.2.1]:53001>',
+                         RequestContext(get_sockaddr('192.0.2.1',
+                                                     53001)).__str__())
+        self.assertEqual('<isc.acl.dns.RequestContext object, ' + \
+                             'remote_addr=[2001:db8::1234]:53006>',
+                         RequestContext(get_sockaddr('2001:db8::1234',
+                                                     53006)).__str__())
+
+        #
+        # Invalid parameters (in our expected usage this should not happen
+        # because the sockaddr would come from the Python socket module, but
+        # validation should still be performed correctly)
+        #
+        # not a tuple
+        self.assertRaises(TypeError, RequestContext, 1)
+        # invalid number of parameters
+        self.assertRaises(TypeError, RequestContext, ('192.0.2.1', 53), 0)
+        # tuple is not in the form of sockaddr
+        self.assertRaises(TypeError, RequestContext, (0, 53))
+        self.assertRaises(TypeError, RequestContext, ('192.0.2.1', 'http'))
+        self.assertRaises(TypeError, RequestContext, ('::', 0, 'flow', 0))
+        # invalid address
+        self.assertRaises(Error, RequestContext, ('example.com', 5300))
+        self.assertRaises(Error, RequestContext, ('192.0.2.1.1', 5300))
+        self.assertRaises(Error, RequestContext, ('2001:db8:::1', 5300))
+
+class RequestACLTest(unittest.TestCase):
+
+    def test_direct_construct(self):
+        self.assertRaises(Error, RequestACL)
+
+    def test_request_loader(self):
+        # these shouldn't raise an exception
+        REQUEST_LOADER.load('[{"action": "DROP"}]')
+        REQUEST_LOADER.load([{"action": "DROP"}])
+        REQUEST_LOADER.load('[{"action": "DROP", "from": "192.0.2.1"}]')
+        REQUEST_LOADER.load([{"action": "DROP", "from": "192.0.2.1"}])
+
+        # Invalid types (note that arguments like '1' or '[]' is of valid
+        # 'type' (but syntax error at a higher level)).  So we need to use
+        # something that is not really JSON nor string.
+        self.assertRaises(TypeError, REQUEST_LOADER.load, b'')
+
+        # Incorrect number of arguments
+        self.assertRaises(TypeError, REQUEST_LOADER.load,
+                          '[{"action": "DROP"}]', 0)
+
+    def test_bad_acl_syntax(self):
+        # the following are derived from loader_test.cc
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '{}');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, {});
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '42');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, 42);
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, 'true');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, True);
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, 'null');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, None);
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '"hello"');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, "hello");
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '[42]');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, [42]);
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '["hello"]');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, ["hello"]);
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '[[]]');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, [[]]);
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '[true]');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, [True]);
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '[null]');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, [None]);
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, '[{}]');
+        self.assertRaises(LoaderError, REQUEST_LOADER.load, [{}]);
+
+        # the following are derived from dns_test.cc
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "ACCEPT", "bad": "192.0.2.1"}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "ACCEPT", "bad": "192.0.2.1"}])
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "ACCEPT", "from": 4}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "ACCEPT", "from": 4}])
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "ACCEPT", "from": []}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "ACCEPT", "from": []}])
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "ACCEPT", "from": "bad"}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "ACCEPT", "from": "bad"}])
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "ACCEPT", "from": null}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "ACCEPT", "from": None}])
+
+    def test_bad_acl_ipsyntax(self):
+        # this test is derived from ip_check_unittest.cc
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "DROP", "from": "192.0.2.43/-1"}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "DROP", "from": "192.0.2.43/-1"}])
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "DROP", "from": "192.0.2.43//1"}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "DROP", "from": "192.0.2.43//1"}])
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "DROP", "from": "192.0.2.43/1/"}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "DROP", "from": "192.0.2.43/1/"}])
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "DROP", "from": "/192.0.2.43/1"}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "DROP", "from": "/192.0.2.43/1"}])
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "DROP", "from": "2001:db8::/xxxx"}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "DROP", "from": "2001:db8::/xxxx"}])
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "DROP", "from": "2001:db8::/32/s"}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "DROP", "from": "2001:db8::/32/s"}])
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "DROP", "from": "1/"}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "DROP", "from": "1/"}])
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "DROP", "from": "/1"}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "DROP", "from": "/1"}])
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "DROP", "from": "192.0.2.0/33"}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "DROP", "from": "192.0.2.0/33"}])
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          '[{"action": "DROP", "from": "::1/129"}]')
+        self.assertRaises(LoaderError, REQUEST_LOADER.load,
+                          [{"action": "DROP", "from": "::1/129"}])
+
+    def test_execute(self):
+        # tests derived from dns_test.cc.  We don't directly expose checks
+        # in the python wrapper, so we test it via execute().
+        self.assertEqual(ACCEPT, get_acl('192.0.2.1').execute(CONTEXT4))
+        self.assertEqual(ACCEPT, get_acl_json('192.0.2.1').execute(CONTEXT4))
+        self.assertEqual(REJECT, get_acl('192.0.2.53').execute(CONTEXT4))
+        self.assertEqual(REJECT, get_acl_json('192.0.2.53').execute(CONTEXT4))
+        self.assertEqual(ACCEPT, get_acl('192.0.2.0/24').execute(CONTEXT4))
+        self.assertEqual(ACCEPT, get_acl_json('192.0.2.0/24').execute(CONTEXT4))
+        self.assertEqual(REJECT, get_acl('192.0.1.0/24').execute(CONTEXT4))
+        self.assertEqual(REJECT, get_acl_json('192.0.1.0/24').execute(CONTEXT4))
+        self.assertEqual(REJECT, get_acl('192.0.1.0/24').execute(CONTEXT4))
+        self.assertEqual(REJECT, get_acl_json('192.0.1.0/24').execute(CONTEXT4))
+
+        self.assertEqual(ACCEPT, get_acl('2001:db8::1').execute(CONTEXT6))
+        self.assertEqual(ACCEPT, get_acl_json('2001:db8::1').execute(CONTEXT6))
+        self.assertEqual(REJECT, get_acl('2001:db8::53').execute(CONTEXT6))
+        self.assertEqual(REJECT, get_acl_json('2001:db8::53').execute(CONTEXT6))
+        self.assertEqual(ACCEPT, get_acl('2001:db8::/64').execute(CONTEXT6))
+        self.assertEqual(ACCEPT,
+                         get_acl_json('2001:db8::/64').execute(CONTEXT6))
+        self.assertEqual(REJECT, get_acl('2001:db8:1::/64').execute(CONTEXT6))
+        self.assertEqual(REJECT,
+                         get_acl_json('2001:db8:1::/64').execute(CONTEXT6))
+        self.assertEqual(REJECT, get_acl('32.1.13.184').execute(CONTEXT6))
+        self.assertEqual(REJECT, get_acl_json('32.1.13.184').execute(CONTEXT6))
+
+        # A bit more complicated example, derived from resolver_config_unittest
+        acl = REQUEST_LOADER.load('[ {"action": "ACCEPT", ' +
+                                  '     "from": "192.0.2.1"},' +
+                                  '    {"action": "REJECT",' +
+                                  '     "from": "192.0.2.0/24"},' +
+                                  '    {"action": "DROP",' +
+                                  '     "from": "2001:db8::1"},' +
+                                  '] }')
+        self.assertEqual(ACCEPT, acl.execute(CONTEXT4))
+        self.assertEqual(REJECT, acl.execute(get_context('192.0.2.2')))
+        self.assertEqual(DROP, acl.execute(get_context('2001:db8::1')))
+        self.assertEqual(REJECT, acl.execute(get_context('2001:db8::2')))
+
+        # same test using the JSON representation
+        acl = REQUEST_LOADER.load([{"action": "ACCEPT", "from": "192.0.2.1"},
+                                   {"action": "REJECT",
+                                    "from": "192.0.2.0/24"},
+                                   {"action": "DROP", "from": "2001:db8::1"}])
+        self.assertEqual(ACCEPT, acl.execute(CONTEXT4))
+        self.assertEqual(REJECT, acl.execute(get_context('192.0.2.2')))
+        self.assertEqual(DROP, acl.execute(get_context('2001:db8::1')))
+        self.assertEqual(REJECT, acl.execute(get_context('2001:db8::2')))
+
+    def test_bad_execute(self):
+        acl = get_acl('192.0.2.1')
+        # missing parameter
+        self.assertRaises(TypeError, acl.execute)
+        # too many parameters
+        self.assertRaises(TypeError, acl.execute, get_context('192.0.2.2'), 0)
+        # type mismatch
+        self.assertRaises(TypeError, acl.execute, 'bad parameter')
+
+class RequestLoaderTest(unittest.TestCase):
+    # Note: loading ACLs is tested in other test cases.
+
+    def test_construct(self):
+        # at least for now, we don't allow direct construction.
+        self.assertRaises(Error, RequestLoader)
+
+if __name__ == '__main__':
+    unittest.main()

+ 12 - 4
src/lib/python/isc/config/Makefile.am

@@ -1,19 +1,27 @@
 SUBDIRS = . tests
 
 python_PYTHON = __init__.py ccsession.py cfgmgr.py config_data.py module_spec.py
-pyexec_DATA = cfgmgr_messages.py
+pyexec_DATA = cfgmgr_messages.py $(top_builddir)/src/lib/python/config_messages.py
 
 pythondir = $(pyexecdir)/isc/config
 
 # Define rule to build logging source files from message file
 cfgmgr_messages.py: cfgmgr_messages.mes
-	$(top_builddir)/src/lib/log/compiler/message -p $(top_srcdir)/src/lib/python/isc/config/cfgmgr_messages.mes
+	$(top_builddir)/src/lib/log/compiler/message \
+	-p $(top_srcdir)/src/lib/python/isc/config/cfgmgr_messages.mes
 
-CLEANFILES = cfgmgr_messages.py cfgmgr_messages.pyc
+$(top_builddir)/src/lib/python/config_messages.py: config_messages.mes
+	$(top_builddir)/src/lib/log/compiler/message \
+		-p -d $(top_builddir)/src/lib/python \
+		$(top_srcdir)/src/lib/python/isc/config/config_messages.mes
+
+CLEANFILES =  cfgmgr_messages.py cfgmgr_messages.pyc
+CLEANFILES += $(top_builddir)/src/lib/python/config_messages.py
+CLEANFILES += $(top_builddir)/src/lib/python/config_messages.pyc
 
 CLEANDIRS = __pycache__
 
-EXTRA_DIST = cfgmgr_messages.mes
+EXTRA_DIST = cfgmgr_messages.mes config_messages.mes
 
 clean-local:
 	rm -rf $(CLEANDIRS)

+ 8 - 10
src/lib/python/isc/config/ccsession.py

@@ -43,6 +43,9 @@ from isc.util.file import path_search
 import bind10_config
 from isc.log import log_config_update
 import json
+from config_messages import *
+
+logger = isc.log.Logger("config")
 
 class ModuleCCSessionError(Exception): pass
 
@@ -127,10 +130,7 @@ def default_logconfig_handler(new_config, config_data):
         isc.log.log_config_update(json.dumps(new_config),
             json.dumps(config_data.get_module_spec().get_full_spec()))
     else:
-        # no logging here yet, TODO: log these errors
-        print("Error in logging configuration, ignoring config update: ")
-        for err in errors:
-            print(err)
+        logger.error(CONFIG_LOG_CONFIG_ERRORS, errors)
 
 class ModuleCCSession(ConfigData):
     """This class maintains a connection to the command channel, as
@@ -142,7 +142,7 @@ class ModuleCCSession(ConfigData):
        callbacks are called when 'check_command' is called on the
        ModuleCCSession"""
        
-    def __init__(self, spec_file_name, config_handler, command_handler, cc_session=None, handle_logging_config=False):
+    def __init__(self, spec_file_name, config_handler, command_handler, cc_session=None, handle_logging_config=True):
         """Initialize a ModuleCCSession. This does *NOT* send the
            specification and request the configuration yet. Use start()
            for that once the ModuleCCSession has been initialized.
@@ -163,7 +163,7 @@ class ModuleCCSession(ConfigData):
            the logger manager to apply it. It will also inform the
            logger manager when the logging configuration gets updated.
            The module does not need to do anything except intializing
-           its loggers, and provide log messages
+           its loggers, and provide log messages. Defaults to true.
         """
         module_spec = isc.config.module_spec_from_file(spec_file_name)
         ConfigData.__init__(self, module_spec)
@@ -385,8 +385,7 @@ class ModuleCCSession(ConfigData):
                                 "Wrong data in configuration: " +
                                 " ".join(errors))
                 else:
-                    # log error
-                    print("[" + self._module_name + "] Error requesting configuration: " + value)
+                    logger.error(CONFIG_GET_FAILED, value)
             else:
                 raise ModuleCCSessionError("No answer from configuration manager")
         except isc.cc.SessionTimeout:
@@ -498,7 +497,6 @@ class UIModuleCCSession(MultiConfigData):
                 self.request_current_config()
                 self.clear_local_changes()
             elif "error" in answer:
-                print("Error: " + answer["error"])
-                print("Configuration not committed")
+                raise ModuleCCSessionError("Error: " + str(answer["error"]) + "\n" + "Configuration not committed")
             else:
                 raise ModuleCCSessionError("Unknown format of answer in commit(): " + str(answer))

+ 33 - 0
src/lib/python/isc/config/config_messages.mes

@@ -0,0 +1,33 @@
+# Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+# No namespace declaration - these constants go in the global namespace
+# of the config_messages python module.
+
+# since these messages are for the python config library, care must
+# be taken that names do not conflict with the messages from the c++
+# config library. A checker script should verify that, but we do not
+# have that at this moment. So when adding a message, make sure that
+# the name is not already used in src/lib/config/config_messages.mes
+
+% CONFIG_LOG_CONFIG_ERRORS error(s) in logging configuration: %1
+There was a logging configuration update, but the internal validator
+for logging configuration found that it contained errors. The errors
+are shown, and the update is ignored.
+
+% CONFIG_GET_FAILED error getting configuration from cfgmgr: %1
+The configuration manager returned an error response when the module
+requested its configuration. The full error message answer from the
+configuration manager is appended to the log error.
+

+ 7 - 2
src/lib/python/isc/config/tests/ccsession_test.py

@@ -23,6 +23,7 @@ from isc.config.ccsession import *
 from isc.config.config_data import BIND10_CONFIG_DATA_VERSION
 from unittest_fakesession import FakeModuleCCSession, WouldBlockForever
 import bind10_config
+import isc.log
 
 class TestHelperFunctions(unittest.TestCase):
     def test_parse_answer(self):
@@ -107,8 +108,11 @@ class TestModuleCCSession(unittest.TestCase):
     def spec_file(self, file):
         return self.data_path + os.sep + file
         
-    def create_session(self, spec_file_name, config_handler = None, command_handler = None, cc_session = None):
-        return ModuleCCSession(self.spec_file(spec_file_name), config_handler, command_handler, cc_session)
+    def create_session(self, spec_file_name, config_handler = None,
+                       command_handler = None, cc_session = None):
+        return ModuleCCSession(self.spec_file(spec_file_name),
+                               config_handler, command_handler,
+                               cc_session, False)
 
     def test_init(self):
         fake_session = FakeModuleCCSession()
@@ -739,5 +743,6 @@ class TestUIModuleCCSession(unittest.TestCase):
         uccs.commit()
 
 if __name__ == '__main__':
+    isc.log.init("bind10")
     unittest.main()
 

+ 11 - 0
src/lib/python/isc/notify/Makefile.am

@@ -1,9 +1,20 @@
 SUBDIRS = . tests
 
 python_PYTHON = __init__.py notify_out.py
+pyexec_DATA = $(top_builddir)/src/lib/python/notify_out_messages.py
 
 pythondir = $(pyexecdir)/isc/notify
 
+$(top_builddir)/src/lib/python/notify_out_messages.py: notify_out_messages.mes
+	$(top_builddir)/src/lib/log/compiler/message \
+		-p -d $(top_builddir)/src/lib/python \
+		$(top_srcdir)/src/lib/python/isc/notify/notify_out_messages.mes
+
+EXTRA_DIST = notify_out_messages.mes
+
+CLEANFILES =  $(top_builddir)/src/lib/python/notify_out_messages.pyc
+CLEANFILES += $(top_builddir)/src/lib/python/notify_out_messages.py
+
 CLEANDIRS = __pycache__
 
 clean-local:

+ 38 - 33
src/lib/python/isc/notify/notify_out.py

@@ -23,11 +23,15 @@ import errno
 from isc.datasrc import sqlite3_ds
 from isc.net import addr
 import isc
-try:
-    from pydnspp import *
-except ImportError as e:
-    # C++ loadable module may not be installed;
-    sys.stderr.write('[b10-xfrout] failed to import DNS or XFR module: %s\n' % str(e))
+from notify_out_messages import *
+
+logger = isc.log.Logger("notify_out")
+
+# there used to be a printed message if this import failed, but if
+# we can't import we should not start anyway, and logging an error
+# is a bad idea since the logging system is most likely not
+# initialized yet. see trac ticket #1103
+from pydnspp import *
 
 ZONE_NEW_DATA_READY_CMD = 'zone_new_data_ready'
 _MAX_NOTIFY_NUM = 30
@@ -46,9 +50,6 @@ _BAD_QR = 4
 _BAD_REPLY_PACKET = 5
 
 SOCK_DATA = b's'
-def addr_to_str(addr):
-    return '%s#%s' % (addr[0], addr[1])
-
 
 class ZoneNotifyInfo:
     '''This class keeps track of notify-out information for one zone.'''
@@ -105,11 +106,10 @@ class NotifyOut:
     notify message to its slaves). notify service can be started by
     calling  dispatcher(), and it can be stoped by calling shutdown()
     in another thread. '''
-    def __init__(self, datasrc_file, log=None, verbose=True):
+    def __init__(self, datasrc_file, verbose=True):
         self._notify_infos = {} # key is (zone_name, zone_class)
         self._waiting_zones = []
         self._notifying_zones = []
-        self._log = log
         self._serving = False
         self._read_sock, self._write_sock = socket.socketpair()
         self._read_sock.setblocking(False)
@@ -362,18 +362,19 @@ class NotifyOut:
         tgt = zone_notify_info.get_current_notify_target()
         if event_type == _EVENT_READ:
             reply = self._get_notify_reply(zone_notify_info.get_socket(), tgt)
-            if reply:
-                if self._handle_notify_reply(zone_notify_info, reply):
+            if reply is not None:
+                if self._handle_notify_reply(zone_notify_info, reply, tgt):
                     self._notify_next_target(zone_notify_info)
 
         elif event_type == _EVENT_TIMEOUT and zone_notify_info.notify_try_num > 0:
-            self._log_msg('info', 'notify retry to %s' % addr_to_str(tgt))
+            logger.info(NOTIFY_OUT_TIMEOUT, tgt[0], tgt[1])
 
         tgt = zone_notify_info.get_current_notify_target()
         if tgt:
             zone_notify_info.notify_try_num += 1
             if zone_notify_info.notify_try_num > _MAX_NOTIFY_TRY_NUM:
-                self._log_msg('info', 'notify to %s: retried exceeded' % addr_to_str(tgt))
+                logger.warn(NOTIFY_OUT_RETRY_EXCEEDED, tgt[0], tgt[1],
+                            _MAX_NOTIFY_TRY_NUM)
                 self._notify_next_target(zone_notify_info)
             else:
                 # set exponential backoff according rfc1996 section 3.6
@@ -412,10 +413,15 @@ class NotifyOut:
         try:
             sock = zone_notify_info.create_socket(addrinfo[0])
             sock.sendto(render.get_data(), 0, addrinfo)
-            self._log_msg('info', 'sending notify to %s' % addr_to_str(addrinfo))
+            logger.info(NOTIFY_OUT_SENDING_NOTIFY, addrinfo[0],
+                        addrinfo[1])
         except (socket.error, addr.InvalidAddress) as err:
-            self._log_msg('error', 'send notify to %s failed: %s' %
-                          (addr_to_str(addrinfo), str(err)))
+            logger.error(NOTIFY_OUT_SOCKET_ERROR, addrinfo[0],
+                         addrinfo[1], err)
+            return False
+        except addr.InvalidAddress as iae:
+            logger.error(NOTIFY_OUT_INVALID_ADDRESS, addrinfo[0],
+                         addrinfo[1], iae)
             return False
 
         return True
@@ -446,34 +452,38 @@ class NotifyOut:
         msg.add_rrset(Message.SECTION_ANSWER, rrset_soa)
         return msg, qid
 
-    def _handle_notify_reply(self, zone_notify_info, msg_data):
+    def _handle_notify_reply(self, zone_notify_info, msg_data, from_addr):
         '''Parse the notify reply message.
-        TODO, the error message should be refined properly.
         rcode will not checked here, If we get the response
         from the slave, it means the slaves has got the notify.'''
         msg = Message(Message.PARSE)
         try:
-            errstr = 'notify reply error: '
             msg.from_wire(msg_data)
             if not msg.get_header_flag(Message.HEADERFLAG_QR):
-                self._log_msg('error', errstr + 'bad flags')
+                logger.warn(NOTIFY_OUT_REPLY_QR_NOT_SET, from_addr[0],
+                            from_addr[1])
                 return _BAD_QR
 
             if msg.get_qid() != zone_notify_info.notify_msg_id:
-                self._log_msg('error', errstr + 'bad query ID')
+                logger.warn(NOTIFY_OUT_REPLY_BAD_QID, from_addr[0],
+                            from_addr[1], msg.get_qid(),
+                            zone_notify_info.notify_msg_id)
                 return _BAD_QUERY_ID
 
             question = msg.get_question()[0]
             if question.get_name() != Name(zone_notify_info.zone_name):
-                self._log_msg('error', errstr + 'bad query name')
+                logger.warn(NOTIFY_OUT_REPLY_BAD_QUERY_NAME, from_addr[0],
+                            from_addr[1], question.get_name().to_text(),
+                            Name(zone_notify_info.zone_name).to_text())
                 return _BAD_QUERY_NAME
 
             if msg.get_opcode() != Opcode.NOTIFY():
-                self._log_msg('error', errstr + 'bad opcode')
+                logger.warn(NOTIFY_OUT_REPLY_BAD_OPCODE, from_addr[0],
+                            from_addr[1], msg.get_opcode().to_text())
                 return _BAD_OPCODE
         except Exception as err:
             # We don't care what exception, just report it?
-            self._log_msg('error', errstr + str(err))
+            logger.error(NOTIFY_OUT_REPLY_UNCAUGHT_EXCEPTION, err)
             return _BAD_REPLY_PACKET
 
         return _REPLY_OK
@@ -481,14 +491,9 @@ class NotifyOut:
     def _get_notify_reply(self, sock, tgt_addr):
         try:
             msg, addr = sock.recvfrom(512)
-        except socket.error:
-            self._log_msg('error', "notify to %s failed: can't read notify reply" % addr_to_str(tgt_addr))
+        except socket.error as err:
+            logger.error(NOTIFY_OUT_SOCKET_RECV_ERROR, tgt_addr[0],
+                         tgt_addr[1], err)
             return None
 
         return msg
-
-
-    def _log_msg(self, level, msg):
-        if self._log:
-            self._log.log_message(level, msg)
-

+ 83 - 0
src/lib/python/isc/notify/notify_out_messages.mes

@@ -0,0 +1,83 @@
+# Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+# No namespace declaration - these constants go in the global namespace
+# of the notify_out_messages python module.
+
+% NOTIFY_OUT_INVALID_ADDRESS invalid address %1#%2: %3
+The notify_out library tried to send a notify message to the given
+address, but it appears to be an invalid address. The configuration
+for secondary nameservers might contain a typographic error, or a
+different BIND 10 module has forgotten to validate its data before
+sending this module a notify command. As such, this should normally
+not happen, and points to an oversight in a different module.
+
+% NOTIFY_OUT_REPLY_BAD_OPCODE bad opcode in notify reply from %1#%2: %3
+The notify_out library sent a notify message to the nameserver at
+the given address, but the response did not have the opcode set to
+NOTIFY. The opcode in the response is printed. Since there was a
+response, no more notifies will be sent to this server for this
+notification event.
+
+% NOTIFY_OUT_REPLY_BAD_QID bad QID in notify reply from %1#%2: got %3, should be %4
+The notify_out library sent a notify message to the nameserver at
+the given address, but the query id in the response does not match
+the one we sent. Since there was a response, no more notifies will
+be sent to this server for this notification event.
+
+% NOTIFY_OUT_REPLY_BAD_QUERY_NAME bad query name in notify reply from %1#%2: got %3, should be %4
+The notify_out library sent a notify message to the nameserver at
+the given address, but the query name in the response does not match
+the one we sent. Since there was a response, no more notifies will
+be sent to this server for this notification event.
+
+% NOTIFY_OUT_REPLY_QR_NOT_SET QR flags set to 0 in reply to notify from %1#%2
+The notify_out library sent a notify message to the namesever at the
+given address, but the reply did not have the QR bit set to one.
+Since there was a response, no more notifies will be sent to this
+server for this notification event.
+
+% NOTIFY_OUT_RETRY_EXCEEDED notify to %1#%2: number of retries (%3) exceeded
+The maximum number of retries for the notify target has been exceeded.
+Either the address of the secondary nameserver is wrong, or it is not
+responding.
+
+% NOTIFY_OUT_SENDING_NOTIFY sending notify to %1#%2
+A notify message is sent to the secondary nameserver at the given
+address.
+
+% NOTIFY_OUT_SOCKET_ERROR socket error sending notify to %1#%2: %3
+There was a network error while trying to send a notify message to
+the given address. The address might be unreachable. The socket
+error is printed and should provide more information.
+
+% NOTIFY_OUT_SOCKET_RECV_ERROR socket error reading notify reply from %1#%2: %3
+There was a network error while trying to read a notify reply
+message from the given address. The socket error is printed and should
+provide more information.
+
+% NOTIFY_OUT_TIMEOUT retry notify to %1#%2
+The notify message to the given address (noted as address#port) has
+timed out, and the message will be resent until the max retry limit
+is reached.
+
+% NOTIFY_OUT_REPLY_UNCAUGHT_EXCEPTION uncaught exception: %1
+There was an uncaught exception in the handling of a notify reply
+message, either in the message parser, or while trying to extract data
+from the parsed message. The error is printed, and notify_out will
+treat the response as a bad message, but this does point to a
+programming error, since all exceptions should have been caught
+explicitely. Please file a bug report. Since there was a response,
+no more notifies will be sent to this server for this notification
+event.

+ 18 - 7
src/lib/python/isc/notify/tests/notify_out_test.py

@@ -21,6 +21,7 @@ import time
 import socket
 from isc.datasrc import sqlite3_ds
 from isc.notify import notify_out, SOCK_DATA
+import isc.log
 
 # our fake socket, where we can read and insert messages
 class MockSocket():
@@ -79,7 +80,6 @@ class TestZoneNotifyInfo(unittest.TestCase):
         self.info.prepare_notify_out()
         self.assertEqual(self.info.get_current_notify_target(), ('127.0.0.1', 53))
 
-        self.assertEqual('127.0.0.1#53', notify_out.addr_to_str(('127.0.0.1', 53)))
         self.info.set_next_notify_target()
         self.assertEqual(self.info.get_current_notify_target(), ('1.1.1.1', 5353))
         self.info.set_next_notify_target()
@@ -223,29 +223,30 @@ class TestNotifyOut(unittest.TestCase):
         self.assertEqual(0, len(self._notify._waiting_zones))
 
     def test_handle_notify_reply(self):
-        self.assertEqual(notify_out._BAD_REPLY_PACKET, self._notify._handle_notify_reply(None, b'badmsg'))
+        fake_address = ('192.0.2.1', 53)
+        self.assertEqual(notify_out._BAD_REPLY_PACKET, self._notify._handle_notify_reply(None, b'badmsg', fake_address))
         example_com_info = self._notify._notify_infos[('example.com.', 'IN')]
         example_com_info.notify_msg_id = 0X2f18
 
         # test with right notify reply message
         data = b'\x2f\x18\xa0\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\03com\x00\x00\x06\x00\x01'
-        self.assertEqual(notify_out._REPLY_OK, self._notify._handle_notify_reply(example_com_info, data))
+        self.assertEqual(notify_out._REPLY_OK, self._notify._handle_notify_reply(example_com_info, data, fake_address))
 
         # test with unright query id
         data = b'\x2e\x18\xa0\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\03com\x00\x00\x06\x00\x01'
-        self.assertEqual(notify_out._BAD_QUERY_ID, self._notify._handle_notify_reply(example_com_info, data))
+        self.assertEqual(notify_out._BAD_QUERY_ID, self._notify._handle_notify_reply(example_com_info, data, fake_address))
 
         # test with unright query name
         data = b'\x2f\x18\xa0\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\03net\x00\x00\x06\x00\x01'
-        self.assertEqual(notify_out._BAD_QUERY_NAME, self._notify._handle_notify_reply(example_com_info, data))
+        self.assertEqual(notify_out._BAD_QUERY_NAME, self._notify._handle_notify_reply(example_com_info, data, fake_address))
 
         # test with unright opcode
         data = b'\x2f\x18\x80\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\03com\x00\x00\x06\x00\x01'
-        self.assertEqual(notify_out._BAD_OPCODE, self._notify._handle_notify_reply(example_com_info, data))
+        self.assertEqual(notify_out._BAD_OPCODE, self._notify._handle_notify_reply(example_com_info, data, fake_address))
 
         # test with unright qr
         data = b'\x2f\x18\x10\x10\x00\x01\x00\x00\x00\x00\x00\x00\x07example\03com\x00\x00\x06\x00\x01'
-        self.assertEqual(notify_out._BAD_QR, self._notify._handle_notify_reply(example_com_info, data))
+        self.assertEqual(notify_out._BAD_QR, self._notify._handle_notify_reply(example_com_info, data, fake_address))
 
     def test_send_notify_message_udp_ipv4(self):
         example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
@@ -300,6 +301,15 @@ class TestNotifyOut(unittest.TestCase):
         self._notify._zone_notify_handler(example_net_info, notify_out._EVENT_NONE)
         self.assertNotEqual(cur_tgt, example_net_info._notify_current)
 
+        cur_tgt = example_net_info._notify_current
+        example_net_info.create_socket('127.0.0.1')
+        # dns message, will result in bad_qid, but what we are testing
+        # here is whether handle_notify_reply is called correctly
+        example_net_info._sock.remote_end().send(b'\x2f\x18\xa0\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\03com\x00\x00\x06\x00\x01')
+        self._notify._zone_notify_handler(example_net_info, notify_out._EVENT_READ)
+        self.assertNotEqual(cur_tgt, example_net_info._notify_current)
+
+
     def _example_net_data_reader(self):
         zone_data = [
         ('example.net.',         '1000',  'IN',  'SOA', 'a.dns.example.net. mail.example.net. 1 1 1 1 1'),
@@ -406,6 +416,7 @@ class TestNotifyOut(unittest.TestCase):
         self.assertFalse(thread.is_alive())
 
 if __name__== "__main__":
+    isc.log.init("bind10")
     unittest.main()
 
 

+ 7 - 7
src/lib/resolve/resolve_messages.mes

@@ -123,11 +123,11 @@ called because a nameserver has been found, and that a query is being sent
 to the specified nameserver.
 
 % RESLIB_TEST_SERVER setting test server to %1(%2)
-This is an internal debugging message and is only generated in unit tests.
-It indicates that all upstream queries from the resolver are being routed to
-the specified server, regardless of the address of the nameserver to which
-the query would normally be routed.  As it should never be seen in normal
-operation, it is a warning message instead of a debug message.
+This is a warning message only generated in unit tests.  It indicates
+that all upstream queries from the resolver are being routed to the
+specified server, regardless of the address of the nameserver to which
+the query would normally be routed.  If seen during normal operation,
+please submit a bug report.
 
 % RESLIB_TEST_UPSTREAM sending upstream query for <%1> to test server at %2
 This is a debug message and should only be seen in unit tests.  A query for
@@ -135,8 +135,8 @@ the specified <name, class, type> tuple is being sent to a test nameserver
 whose address is given in the message.
 
 % RESLIB_TIMEOUT query <%1> to %2 timed out
-A debug message indicating that the specified query has timed out and as
-there are no retries left, an error will be reported.
+A debug message indicating that the specified upstream query has timed out and
+there are no retries left.
 
 % RESLIB_TIMEOUT_RETRY query <%1> to %2 timed out, re-trying (retries left: %3)
 A debug message indicating that the specified query has timed out and that

+ 2 - 1
src/lib/server_common/tests/keyring_test.cc

@@ -38,7 +38,8 @@ public:
         specfile(std::string(TEST_DATA_PATH) + "/spec.spec")
     {
         session.getMessages()->add(createAnswer());
-        mccs.reset(new ModuleCCSession(specfile, session, NULL, NULL, false));
+        mccs.reset(new ModuleCCSession(specfile, session, NULL, NULL,
+                                       false, false));
     }
     isc::cc::FakeSession session;
     std::auto_ptr<ModuleCCSession> mccs;

+ 18 - 0
src/lib/util/filename.cc

@@ -132,6 +132,24 @@ Filename::useAsDefault(const string& name) const {
     return (retstring);
 }
 
+void
+Filename::setDirectory(const std::string& new_directory) {
+    std::string directory(new_directory);
+
+    if (directory.length() > 0) {
+        // append '/' if necessary
+        size_t sep = directory.rfind('/');
+        if (sep == std::string::npos || sep < directory.size() - 1) {
+            directory += "/";
+        }
+    }
+    // and regenerate the full name
+    std::string full_name = directory + name_ + extension_;
+
+    directory_.swap(directory);
+    full_name_.swap(full_name);
+}
+
 
 } // namespace log
 } // namespace isc

+ 7 - 0
src/lib/util/filename.h

@@ -86,6 +86,13 @@ public:
         return (directory_);
     }
 
+    /// \brief Set directory for the file
+    ///
+    /// \param new_directory The directory to set. If this is an empty
+    ///        string, the directory this filename object currently
+    ///        has will be removed.
+    void setDirectory(const std::string& new_directory);
+
     /// \return Name of Given File Name
     std::string name() const {
         return (name_);

+ 28 - 1
src/lib/util/python/pycppwrapper_util.h

@@ -94,6 +94,22 @@ public:
 /// the reference to be decreased, the original bare pointer should be
 /// extracted using the \c release() method.
 ///
+/// In some other cases, it would be convenient if it's possible to create
+/// an "empty" container and reset it with a Python object later.
+/// For example, we may want to create a temporary Python object in the
+/// middle of a function and make sure that it's valid within the rest of
+/// the function scope, while we want to make sure its reference is released
+/// when the function returns (either normally or as a result of exception).
+/// To allow this scenario, this class defines the default constructor
+/// and the \c reset() method.  The default constructor allows the class
+/// object with an "empty" (NULL) Python object, while \c reset() allows
+/// the stored object to be replaced with a new one.  If there's a valid
+/// object was already set, \c reset() releases its reference.
+/// In general, it's safer to construct the container object with a valid
+/// Python object pointer.  The use of the default constructor and
+/// \c reset() should therefore be restricted to cases where it's
+/// absolutely necessary.
+///
 /// There are two convenience methods for commonly used operations:
 /// \c installAsClassVariable() to add the PyObject as a class variable
 /// and \c installToModule to add the PyObject to a specified python module.
@@ -166,16 +182,27 @@ public:
 /// exception in a python biding written in C/C++.  See the code comment
 /// of the method for more details.
 struct PyObjectContainer {
+    PyObjectContainer() : obj_(NULL) {}
     PyObjectContainer(PyObject* obj) : obj_(obj) {
         if (obj_ == NULL) {
             isc_throw(PyCPPWrapperException, "Unexpected NULL PyObject, "
                       "probably due to short memory");
         }
     }
-    virtual ~PyObjectContainer() {
+    ~PyObjectContainer() {
+        if (obj_ != NULL) {
+            Py_DECREF(obj_);
+        }
+    }
+    void reset(PyObject* obj) {
+        if (obj == NULL) {
+            isc_throw(PyCPPWrapperException, "Unexpected NULL PyObject, "
+                      "probably due to short memory");
+        }
         if (obj_ != NULL) {
             Py_DECREF(obj_);
         }
+        obj_ = obj;
     }
     PyObject* get() {
         return (obj_);

+ 2 - 2
src/lib/util/python/wrapper_template.cc

@@ -210,7 +210,7 @@ namespace python {
 // Most of the functions are not actually implemented and NULL here.
 PyTypeObject @cppclass@_type = {
     PyVarObject_HEAD_INIT(NULL, 0)
-    "pydnspp.@CPPCLASS@",
+    "@MODULE@.@CPPCLASS@",
     sizeof(s_@CPPCLASS@),                 // tp_basicsize
     0,                                  // tp_itemsize
     reinterpret_cast<destructor>(@CPPCLASS@_destroy),       // tp_dealloc
@@ -222,7 +222,7 @@ PyTypeObject @cppclass@_type = {
     NULL,                               // tp_as_number
     NULL,                               // tp_as_sequence
     NULL,                               // tp_as_mapping
-    NULL,                               // tp_hash 
+    NULL,                               // tp_hash
     NULL,                               // tp_call
     // THIS MAY HAVE TO BE CHANGED TO NULL:
     @CPPCLASS@_str,                       // tp_str

+ 37 - 0
src/lib/util/tests/filename_unittest.cc

@@ -177,3 +177,40 @@ TEST_F(FilenameTest, UseAsDefault) {
     EXPECT_EQ("/s/t/u", fname.useAsDefault("/s/t/u"));
     EXPECT_EQ("/a/b/c", fname.useAsDefault(""));
 }
+
+TEST_F(FilenameTest, setDirectory) {
+    Filename fname("a.b");
+    EXPECT_EQ("", fname.directory());
+    EXPECT_EQ("a.b", fname.fullName());
+    EXPECT_EQ("a.b", fname.expandWithDefault(""));
+
+    fname.setDirectory("/just/some/dir/");
+    EXPECT_EQ("/just/some/dir/", fname.directory());
+    EXPECT_EQ("/just/some/dir/a.b", fname.fullName());
+    EXPECT_EQ("/just/some/dir/a.b", fname.expandWithDefault(""));
+
+    fname.setDirectory("/just/some/dir");
+    EXPECT_EQ("/just/some/dir/", fname.directory());
+    EXPECT_EQ("/just/some/dir/a.b", fname.fullName());
+    EXPECT_EQ("/just/some/dir/a.b", fname.expandWithDefault(""));
+
+    fname.setDirectory("/");
+    EXPECT_EQ("/", fname.directory());
+    EXPECT_EQ("/a.b", fname.fullName());
+    EXPECT_EQ("/a.b", fname.expandWithDefault(""));
+
+    fname.setDirectory("");
+    EXPECT_EQ("", fname.directory());
+    EXPECT_EQ("a.b", fname.fullName());
+    EXPECT_EQ("a.b", fname.expandWithDefault(""));
+
+    fname = Filename("/first/a.b");
+    EXPECT_EQ("/first/", fname.directory());
+    EXPECT_EQ("/first/a.b", fname.fullName());
+    EXPECT_EQ("/first/a.b", fname.expandWithDefault(""));
+
+    fname.setDirectory("/just/some/dir");
+    EXPECT_EQ("/just/some/dir/", fname.directory());
+    EXPECT_EQ("/just/some/dir/a.b", fname.fullName());
+    EXPECT_EQ("/just/some/dir/a.b", fname.expandWithDefault(""));
+}