Browse Source

[1457] Merge branch 'master' into merge_1457

and updated tests to reflect changed initializer for UpdateSession
Conflicts:
	src/lib/python/isc/ddns/libddns_messages.mes
	src/lib/python/isc/ddns/session.py
	src/lib/python/isc/ddns/tests/session_tests.py
Jelte Jansen 13 years ago
parent
commit
5122d4095a
35 changed files with 686 additions and 3748 deletions
  1. 6 0
      ChangeLog
  2. 1 1
      src/bin/bind10/bind10.8
  3. 1 1
      src/bin/bind10/bind10.xml
  4. 0 1
      src/bin/bindctl/command_sets.py
  5. 1 1
      src/bin/cfgmgr/b10-cfgmgr.8
  6. 1 1
      src/bin/cfgmgr/b10-cfgmgr.xml
  7. 0 1
      src/bin/cfgmgr/tests/b10-cfgmgr_test.py.in
  8. 1 1
      src/bin/dbutil/dbutil.py.in
  9. 2 0
      src/bin/ddns/tests/ddns_test.py
  10. 19 6
      src/bin/dhcp4/main.cc
  11. 15 2
      src/bin/dhcp4/tests/Makefile.am
  12. 170 0
      src/bin/dhcp4/tests/dhcp4_test.py
  13. 1 1
      src/bin/dhcp6/dhcp6_srv.cc
  14. 17 6
      src/bin/dhcp6/main.cc
  15. 3 2
      src/bin/dhcp6/tests/Makefile.am
  16. 127 32
      src/bin/dhcp6/tests/dhcp6_test.py
  17. 1 1
      src/bin/sockcreator/Makefile.am
  18. 1 1
      src/lib/cc/tests/Makefile.am
  19. 1 1
      src/lib/config/module_spec.cc
  20. 1 1
      src/lib/datasrc/rbnode_rrset.h
  21. 0 1
      src/lib/dns/rdata/generic/detail/nsec3param_common.cc
  22. 1 1
      src/lib/python/isc/config/cfgmgr.py
  23. 5 4
      src/lib/python/isc/config/cfgmgr_messages.mes
  24. 24 8
      src/lib/python/isc/ddns/libddns_messages.mes
  25. 13 4
      src/lib/python/isc/ddns/logger.py
  26. 45 22
      src/lib/python/isc/ddns/session.py
  27. 1 1
      src/lib/python/isc/ddns/tests/Makefile.am
  28. 121 60
      src/lib/python/isc/ddns/tests/session_tests.py
  29. 53 4
      src/lib/python/isc/ddns/tests/zone_config_tests.py
  30. 38 1
      src/lib/python/isc/ddns/zone_config.py
  31. 1 1
      src/lib/util/locks.h
  32. 8 2
      src/lib/xfr/xfrout_client.cc
  33. 1 1
      tests/lettuce/features/bindctl_commands.feature
  34. 6 13
      tests/tools/perfdhcp/Makefile.am
  35. 0 3565
      tests/tools/perfdhcp/perfdhcp.cc

+ 6 - 0
ChangeLog

@@ -1,3 +1,9 @@
+442.	[func]		tomek
+	b10-dhcp4, b10-dhcp6: Both DHCP servers now accept -p parameter
+	that can be used to specify listening port number. This capability
+	is useful only for testing purposes.
+	(Trac #1503, git e60af9fa16a6094d2204f27c40a648fae313bdae)
+
 441.	[func]		tomek
 441.	[func]		tomek
 	libdhcp++: Stub interface detection (support for interfaces.txt
 	libdhcp++: Stub interface detection (support for interfaces.txt
 	file) was removed.
 	file) was removed.

+ 1 - 1
src/bin/bind10/bind10.8

@@ -42,7 +42,7 @@ b10\-config\&.db\&.
 .RS 4
 .RS 4
 This will create a backup of the existing configuration file, remove it and start
 This will create a backup of the existing configuration file, remove it and start
 b10\-cfgmgr(8)
 b10\-cfgmgr(8)
-with the default configuration\&. The name of the backup file can be found in the logs (\fICFGMGR_RENAMED_CONFIG_FILE\fR)\&. (It will append a number to the backup filename if a previous backup file exists\&.)
+with the default configuration\&. The name of the backup file can be found in the logs (\fICFGMGR_BACKED_UP_CONFIG_FILE\fR)\&. (It will append a number to the backup filename if a previous backup file exists\&.)
 .RE
 .RE
 .PP
 .PP
 \fB\-\-cmdctl\-port\fR \fIport\fR
 \fB\-\-cmdctl\-port\fR \fIport\fR

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

@@ -116,7 +116,7 @@
 	    <refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum>
 	    <refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum>
             with the default configuration.
             with the default configuration.
 	    The name of the backup file can be found in the logs
 	    The name of the backup file can be found in the logs
-	    (<varname>CFGMGR_RENAMED_CONFIG_FILE</varname>).
+	    (<varname>CFGMGR_BACKED_UP_CONFIG_FILE</varname>).
 	    (It will append a number to the backup filename if a
 	    (It will append a number to the backup filename if a
 	    previous backup file exists.)
 	    previous backup file exists.)
 
 

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

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

+ 1 - 1
src/bin/cfgmgr/b10-cfgmgr.8

@@ -54,7 +54,7 @@ The arguments are as follows:
 .RS 4
 .RS 4
 This will create a backup of the existing configuration file, remove it, and
 This will create a backup of the existing configuration file, remove it, and
 b10\-cfgmgr(8)
 b10\-cfgmgr(8)
-will use the default configurations\&. The name of the backup file can be found in the logs (\fICFGMGR_RENAMED_CONFIG_FILE\fR)\&. (It will append a number to the backup filename if a previous backup file exists\&.)
+will use the default configurations\&. The name of the backup file can be found in the logs (\fICFGMGR_BACKED_UP_CONFIG_FILE\fR)\&. (It will append a number to the backup filename if a previous backup file exists\&.)
 .RE
 .RE
 .PP
 .PP
 \fB\-c\fR \fIconfig\-filename\fR, \fB\-\-config\-filename\fR \fIconfig\-filename\fR
 \fB\-c\fR \fIconfig\-filename\fR, \fB\-\-config\-filename\fR \fIconfig\-filename\fR

+ 1 - 1
src/bin/cfgmgr/b10-cfgmgr.xml

@@ -107,7 +107,7 @@
             <refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum>
             <refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum>
             will use the default configurations.
             will use the default configurations.
             The name of the backup file can be found in the logs
             The name of the backup file can be found in the logs
-            (<varname>CFGMGR_RENAMED_CONFIG_FILE</varname>).
+            (<varname>CFGMGR_BACKED_UP_CONFIG_FILE</varname>).
             (It will append a number to the backup filename if a
             (It will append a number to the backup filename if a
             previous backup file exists.)
             previous backup file exists.)
           </para>
           </para>

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

@@ -209,7 +209,6 @@ class TestParseArgs(unittest.TestCase):
         self.assertFalse(parsed.clear_config)
         self.assertFalse(parsed.clear_config)
         parsed = b.parse_options(['--clear-config'], TestOptParser)
         parsed = b.parse_options(['--clear-config'], TestOptParser)
         self.assertTrue(parsed.clear_config)
         self.assertTrue(parsed.clear_config)
-        
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':
     unittest.main()
     unittest.main()

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

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

+ 2 - 0
src/bin/ddns/tests/ddns_test.py

@@ -379,6 +379,8 @@ class TestMain(unittest.TestCase):
 
 
     def __clear_socket(self):
     def __clear_socket(self):
         self.__clear_called = True
         self.__clear_called = True
+        # Get rid of the socket file too
+        self.__orig_clear()
 
 
     def check_exception(self, ex):
     def check_exception(self, ex):
         '''Common test sequence to see if the given exception is caused.
         '''Common test sequence to see if the given exception is caused.

+ 19 - 6
src/bin/dhcp4/main.cc

@@ -37,6 +37,7 @@
 
 
 #include <dhcp4/spec_config.h>
 #include <dhcp4/spec_config.h>
 #include <dhcp4/dhcp4_srv.h>
 #include <dhcp4/dhcp4_srv.h>
+#include <dhcp/dhcp4.h>
 
 
 using namespace std;
 using namespace std;
 using namespace isc::util;
 using namespace isc::util;
@@ -53,33 +54,45 @@ usage() {
     cerr << "Usage:  b10-dhcp4 [-v]"
     cerr << "Usage:  b10-dhcp4 [-v]"
          << endl;
          << endl;
     cerr << "\t-v: verbose output" << endl;
     cerr << "\t-v: verbose output" << endl;
-    exit(1);
+    cerr << "\t-p number: specify non-standard port number 1-65535 (useful for testing only)" << endl;
+    exit(EXIT_FAILURE);
 }
 }
 } // end of anonymous namespace
 } // end of anonymous namespace
 
 
 int
 int
 main(int argc, char* argv[]) {
 main(int argc, char* argv[]) {
     int ch;
     int ch;
+    int port_number = DHCP4_SERVER_PORT; // The default. any other values are
+                                         // useful for testing only.
 
 
-    while ((ch = getopt(argc, argv, ":v")) != -1) {
+    while ((ch = getopt(argc, argv, "vp:")) != -1) {
         switch (ch) {
         switch (ch) {
         case 'v':
         case 'v':
             verbose_mode = true;
             verbose_mode = true;
             isc::log::denabled = true;
             isc::log::denabled = true;
             break;
             break;
+        case 'p':
+            port_number = strtol(optarg, NULL, 10);
+            if (port_number == 0) {
+                cerr << "Failed to parse port number: [" << optarg
+                     << "], 1-65535 allowed." << endl;
+                usage();
+            }
+            break;
         case ':':
         case ':':
         default:
         default:
             usage();
             usage();
         }
         }
     }
     }
 
 
-    cout << "My pid=" << getpid() << endl;
+    cout << "My pid=" << getpid() << ", binding to port " << port_number
+         << ", verbose " << (verbose_mode?"yes":"no") << endl;
 
 
     if (argc - optind > 0) {
     if (argc - optind > 0) {
         usage();
         usage();
     }
     }
 
 
-    int ret = 0;
+    int ret = EXIT_SUCCESS;
 
 
     // TODO remainder of auth to dhcp4 code copy. We need to enable this in
     // TODO remainder of auth to dhcp4 code copy. We need to enable this in
     //      dhcp4 eventually
     //      dhcp4 eventually
@@ -99,13 +112,13 @@ main(int argc, char* argv[]) {
 
 
         cout << "[b10-dhcp4] Initiating DHCPv4 server operation." << endl;
         cout << "[b10-dhcp4] Initiating DHCPv4 server operation." << endl;
 
 
-        Dhcpv4Srv* srv = new Dhcpv4Srv();
+        Dhcpv4Srv* srv = new Dhcpv4Srv(port_number);
 
 
         srv->run();
         srv->run();
 
 
     } catch (const std::exception& ex) {
     } catch (const std::exception& ex) {
         cerr << "[b10-dhcp4] Server failed: " << ex.what() << endl;
         cerr << "[b10-dhcp4] Server failed: " << ex.what() << endl;
-        ret = 1;
+        ret = EXIT_FAILURE;
     }
     }
 
 
     return (ret);
     return (ret);

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

@@ -1,12 +1,25 @@
 PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
 PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
 
 
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
+PYTESTS = dhcp4_test.py
+EXTRA_DIST = $(PYTESTS)
+
+# Explicitly specify paths to dynamic libraries required by loadable python
+# modules. That is required on Mac OS systems. Otherwise we will get exception
+# about python not being able to load liblog library.
 LIBRARY_PATH_PLACEHOLDER =
 LIBRARY_PATH_PLACEHOLDER =
 if SET_ENV_LIBRARY_PATH
 if SET_ENV_LIBRARY_PATH
 LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
 LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
 endif
 endif
 
 
+# test using command-line arguments, so use check-local target instead of TESTS
+check-local:
+	for pytest in $(PYTESTS) ; do \
+	echo Running test: $$pytest ; \
+	PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_srcdir)/src/bin:$(abs_top_builddir)/src/bin/bind10:$(abs_top_builddir)/src/lib/util/io/.libs \
+	$(LIBRARY_PATH_PLACEHOLDER) \
+		$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
+	done
+
 AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
 AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
 AM_CPPFLAGS += -I$(top_builddir)/src/bin # for generated spec_config.h header
 AM_CPPFLAGS += -I$(top_builddir)/src/bin # for generated spec_config.h header
 AM_CPPFLAGS += -I$(top_srcdir)/src/bin
 AM_CPPFLAGS += -I$(top_srcdir)/src/bin

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

@@ -0,0 +1,170 @@
+# Copyright (C) 2012 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+from bind10_src import ProcessInfo, parse_args, dump_pid, unlink_pid_file, _BASETIME
+
+import unittest
+import sys
+import os
+import signal
+import socket
+from isc.net.addr import IPAddr
+import time
+import isc
+import fcntl
+
+class TestDhcpv4Daemon(unittest.TestCase):
+    def setUp(self):
+        # don't redirect stdout/stderr here as we want to print out things
+        # during the test
+        pass
+
+    def tearDown(self):
+        pass
+
+    def runDhcp4(self, params, wait=1):
+        """
+        This method runs dhcp4 and returns a touple: (returncode, stdout, stderr)
+        """
+        ## @todo: Convert this into generic method and reuse it in dhcp6
+
+        print("Running command: %s" % (" ".join(params)))
+
+        # redirect stdout to a pipe so we can check that our
+        # process spawning is doing the right thing with stdout
+        self.stdout_old = os.dup(sys.stdout.fileno())
+        self.stdout_pipes = os.pipe()
+        os.dup2(self.stdout_pipes[1], sys.stdout.fileno())
+        os.close(self.stdout_pipes[1])
+
+        # do the same trick for stderr:
+        self.stderr_old = os.dup(sys.stderr.fileno())
+        self.stderr_pipes = os.pipe()
+        os.dup2(self.stderr_pipes[1], sys.stderr.fileno())
+        os.close(self.stderr_pipes[1])
+
+        # note that we use dup2() to restore the original stdout
+        # to the main program ASAP in each test... this prevents
+        # hangs reading from the child process (as the pipe is only
+        # open in the child), and also insures nice pretty output
+
+        pi = ProcessInfo('Test Process', params)
+        pi.spawn()
+        time.sleep(wait)
+        os.dup2(self.stdout_old, sys.stdout.fileno())
+        os.dup2(self.stderr_old, sys.stderr.fileno())
+        self.assertNotEqual(pi.process, None)
+        self.assertTrue(type(pi.pid) is int)
+
+        # Set non-blocking read on pipes. Process may not print anything
+        # on specific output and the we would hang without this.
+        fd = self.stdout_pipes[0]
+        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+        fd = self.stderr_pipes[0]
+        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+        # There's potential problem if b10-dhcp4 prints out more
+        # than 4k of text
+        try:
+            output = os.read(self.stdout_pipes[0], 4096)
+        except OSError:
+            print("No data available from stdout")
+            output = ""
+
+        # read can return None. Make sure we have a string
+        if (output is None):
+            output = ""
+
+        try:
+            error = os.read(self.stderr_pipes[0], 4096)
+        except OSError:
+            print("No data available on stderr")
+            error = ""
+
+        # read can return None. Make sure we have a string
+        if (error is None):
+            error = ""
+
+
+        try:
+            if (not pi.process.poll()):
+                # let's be nice at first...
+                pi.process.terminate()
+        except OSError:
+            print("Ignoring failed kill attempt. Process is dead already.")
+
+        # call this to get returncode, process should be dead by now
+        rc = pi.process.wait()
+
+        # Clean up our stdout/stderr munging.
+        os.dup2(self.stdout_old, sys.stdout.fileno())
+        os.close(self.stdout_pipes[0])
+
+        os.dup2(self.stderr_old, sys.stderr.fileno())
+        os.close(self.stderr_pipes[0])
+
+        print ("Process finished, return code=%d, stdout=%d bytes, stderr=%d bytes"
+               % (rc, len(output), len(error)) )
+
+        return (rc, output, error)
+
+    def test_alive(self):
+        print("Note: Purpose of some of the tests is to check if DHCPv4 server can be started,")
+        print("      not that is can bind sockets correctly. Please ignore binding errors.")
+
+        (returncode, output, error) = self.runDhcp4(["../b10-dhcp4", "-v"])
+
+        self.assertEqual( str(output).count("[b10-dhcp4] Initiating DHCPv4 server operation."), 1)
+
+    def test_portnumber_0(self):
+        print("Check that specifying port number 0 is not allowed.")
+
+        (returncode, output, error) = self.runDhcp4(['../b10-dhcp4', '-p', '0'])
+
+        # When invalid port number is specified, return code must not be success
+        self.assertTrue(returncode != 0)
+
+        # Check that there is an error message about invalid port number printed on stderr
+        self.assertEqual( str(error).count("Failed to parse port number"), 1)
+
+    def test_portnumber_missing(self):
+        print("Check that -p option requires a parameter.")
+
+        (returncode, output, error) = self.runDhcp4(['../b10-dhcp4', '-p'])
+
+        # When invalid port number is specified, return code must not be success
+        self.assertTrue(returncode != 0)
+
+        # Check that there is an error message about invalid port number printed on stderr
+        self.assertEqual( str(error).count("option requires an argument"), 1)
+
+    def test_portnumber_nonroot(self):
+        print("Check that specifying unprivileged port number will work.")
+
+        (returncode, output, error) = self.runDhcp4(['../b10-dhcp4', '-p', '10057'])
+
+        # When invalid port number is specified, return code must not be success
+        # TODO: Temporarily commented out as socket binding on systems that do not have
+        #       interface detection implemented currently fails.
+        # self.assertTrue(returncode == 0)
+
+        # Check that there is an error message about invalid port number printed on stderr
+        self.assertEqual( str(output).count("opening sockets on port 10057"), 1)
+
+if __name__ == '__main__':
+    unittest.main()

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

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

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

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

+ 3 - 2
src/bin/dhcp6/tests/Makefile.am

@@ -2,8 +2,9 @@ PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
 PYTESTS = dhcp6_test.py
 PYTESTS = dhcp6_test.py
 EXTRA_DIST = $(PYTESTS)
 EXTRA_DIST = $(PYTESTS)
 
 
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
+# Explicitly specify paths to dynamic libraries required by loadable python
+# modules. That is required on Mac OS systems. Otherwise we will get exception
+# about python not being able to load liblog library.
 LIBRARY_PATH_PLACEHOLDER =
 LIBRARY_PATH_PLACEHOLDER =
 if SET_ENV_LIBRARY_PATH
 if SET_ENV_LIBRARY_PATH
 LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
 LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)

+ 127 - 32
src/bin/dhcp6/tests/dhcp6_test.py

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

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

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

+ 1 - 1
src/lib/cc/tests/Makefile.am

@@ -26,7 +26,7 @@ run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
 
 
 # We need to put our libs first, in case gtest (or any dependency, really)
 # We need to put our libs first, in case gtest (or any dependency, really)
 # is installed in the same location as a different version of bind10
 # is installed in the same location as a different version of bind10
-# Otherwise the linker may not use the source tree libs 
+# Otherwise the linker may not use the source tree libs
 run_unittests_LDADD =  $(top_builddir)/src/lib/cc/libcc.la
 run_unittests_LDADD =  $(top_builddir)/src/lib/cc/libcc.la
 run_unittests_LDADD +=  $(top_builddir)/src/lib/log/liblog.la
 run_unittests_LDADD +=  $(top_builddir)/src/lib/log/liblog.la
 run_unittests_LDADD +=  $(top_builddir)/src/lib/util/unittests/libutil_unittests.la
 run_unittests_LDADD +=  $(top_builddir)/src/lib/util/unittests/libutil_unittests.la

+ 1 - 1
src/lib/config/module_spec.cc

@@ -136,7 +136,7 @@ check_statistics_item_list(ConstElementPtr spec) {
             && item->contains("item_default")) {
             && item->contains("item_default")) {
             if(!check_format(item->get("item_default"),
             if(!check_format(item->get("item_default"),
                              item->get("item_format"))) {
                              item->get("item_format"))) {
-                isc_throw(ModuleSpecError, 
+                isc_throw(ModuleSpecError,
                     "item_default not valid type of item_format");
                     "item_default not valid type of item_format");
             }
             }
         }
         }

+ 1 - 1
src/lib/datasrc/rbnode_rrset.h

@@ -81,7 +81,7 @@ struct AdditionalNodeInfo;
 /// can refer to its definition, and only for that purpose.  Otherwise this is
 /// can refer to its definition, and only for that purpose.  Otherwise this is
 /// essentially a private class of the in-memory data source implementation,
 /// essentially a private class of the in-memory data source implementation,
 /// and an application shouldn't directly refer to this class.
 /// and an application shouldn't directly refer to this class.
-/// 
+///
 // Note: non-Doxygen-documented methods are documented in the base class.
 // Note: non-Doxygen-documented methods are documented in the base class.
 
 
 class RBNodeRRset : public isc::dns::AbstractRRset {
 class RBNodeRRset : public isc::dns::AbstractRRset {

+ 0 - 1
src/lib/dns/rdata/generic/detail/nsec3param_common.cc

@@ -127,4 +127,3 @@ parseNSEC3ParamWire(const char* const rrtype_name,
 } // end of rdata
 } // end of rdata
 } // end of dns
 } // end of dns
 } // end of isc
 } // end of isc
-

+ 1 - 1
src/lib/python/isc/config/cfgmgr.py

@@ -167,7 +167,7 @@ class ConfigManagerData:
                 i += 1
                 i += 1
             new_file_name = new_file_name + "." + str(i)
             new_file_name = new_file_name + "." + str(i)
         if os.path.exists(old_file_name):
         if os.path.exists(old_file_name):
-            logger.info(CFGMGR_RENAMED_CONFIG_FILE, old_file_name, new_file_name)
+            logger.info(CFGMGR_BACKED_UP_CONFIG_FILE, old_file_name, new_file_name)
             os.rename(old_file_name, new_file_name)
             os.rename(old_file_name, new_file_name)
 
 
     def __eq__(self, other):
     def __eq__(self, other):

+ 5 - 4
src/lib/python/isc/config/cfgmgr_messages.mes

@@ -55,10 +55,11 @@ error is given. The most likely cause is that the system does not have
 write access to the configuration database file. The updated
 write access to the configuration database file. The updated
 configuration is not stored.
 configuration is not stored.
 
 
-% CFGMGR_RENAMED_CONFIG_FILE renamed configuration file %1 to %2, will create new %1
-BIND 10 has been started with the command to clear the configuration file.
-The existing file is backed up to the given file name, so that data is not
-immediately lost if this was done by accident.
+% CFGMGR_BACKED_UP_CONFIG_FILE Config file %1 was removed; a backup was made at %2
+BIND 10 has been started with the command to clear the configuration
+file.  The existing file has been backed up (moved) to the given file
+name. A new configuration file will be created in the original location
+when necessary.
 
 
 % CFGMGR_STOPPED_BY_KEYBOARD keyboard interrupt, shutting down
 % CFGMGR_STOPPED_BY_KEYBOARD keyboard interrupt, shutting down
 There was a keyboard interrupt signal to stop the cfgmgr daemon. The
 There was a keyboard interrupt signal to stop the cfgmgr daemon. The

+ 24 - 8
src/lib/python/isc/ddns/libddns_messages.mes

@@ -100,6 +100,10 @@ record has an RRType that is considered a 'meta' type, which
 cannot be zone content data. The specific record is shown.
 cannot be zone content data. The specific record is shown.
 A FORMERR response is sent back to the client.
 A FORMERR response is sent back to the client.
 
 
+% LIBDDNS_UPDATE_APPROVED update client %1 for zone %2 approved
+Debug message.  An update request was approved in terms of the zone's
+update ACL.
+
 % LIBDDNS_UPDATE_BAD_CLASS update client %1 for zone %2: bad class in update RR: %3
 % LIBDDNS_UPDATE_BAD_CLASS update client %1 for zone %2: bad class in update RR: %3
 The Update section of a DDNS update message contains an RRset with
 The Update section of a DDNS update message contains an RRset with
 a bad class. The class of the update RRset must be either the same
 a bad class. The class of the update RRset must be either the same
@@ -140,6 +144,18 @@ The Update section of a DDNS update message contains a 'delete rrs'
 statement with a non-zero TTL. This is not allowed by the protocol.
 statement with a non-zero TTL. This is not allowed by the protocol.
 A FORMERR response is sent back to the client.
 A FORMERR response is sent back to the client.
 
 
+% LIBDDNS_UPDATE_DENIED update client %1 for zone %2 denied
+Informational message.  An update request was denied because it was
+rejected by the zone's update ACL.  When this library is used by
+b10-ddns, the server will respond to the request with an RCODE of
+REFUSED as described in Section 3.3 of RFC2136.
+
+% LIBDDNS_UPDATE_DROPPED update client %1 for zone %2 dropped
+Informational message.  An update request was denied because it was
+rejected by the zone's update ACL.  When this library is used by
+b10-ddns, the server will then completely ignore the request; no
+response will be sent.
+
 % LIBDDNS_UPDATE_ERROR update client %1 for zone %2: %3
 % LIBDDNS_UPDATE_ERROR update client %1 for zone %2: %3
 Debug message.  An error is found in processing a dynamic update
 Debug message.  An error is found in processing a dynamic update
 request.  This log message is used for general errors that are not
 request.  This log message is used for general errors that are not
@@ -156,14 +172,14 @@ will simply return a response with an RCODE of NOTIMP to the client.
 The client's address and the zone name/class are logged.
 The client's address and the zone name/class are logged.
 
 
 % LIBDDNS_UPDATE_NOTAUTH update client %1 for zone %2: not authoritative for update zone
 % LIBDDNS_UPDATE_NOTAUTH update client %1 for zone %2: not authoritative for update zone
-Debug message.  An update request for a zone for which the receiving
-server doesn't have authority.  In theory this is an unexpected event,
-but there are client implementations that could send update requests
-carelessly, so it may not necessarily be so uncommon in practice.  If
-possible, you may want to check the implementation or configuration of
-those clients to suppress the requests.  As specified in Section 3.1
-of RFC2136, the receiving server will return a response with an RCODE
-of NOTAUTH.
+Debug message.  An update request was received for a zone for which
+the receiving server doesn't have authority.  In theory this is an
+unexpected event, but there are client implementations that could send
+update requests carelessly, so it may not necessarily be so uncommon
+in practice.  If possible, you may want to check the implementation or
+configuration of those clients to suppress the requests.  As specified
+in Section 3.1 of RFC2136, the receiving server will return a response
+with an RCODE of NOTAUTH.
 
 
 % LIBDDNS_UPDATE_NOTZONE update client %1 for zone %2: update RR out of zone %3
 % LIBDDNS_UPDATE_NOTZONE update client %1 for zone %2: update RR out of zone %3
 A DDNS UPDATE record has a name that does not appear to be inside
 A DDNS UPDATE record has a name that does not appear to be inside

+ 13 - 4
src/lib/python/isc/ddns/logger.py

@@ -28,9 +28,11 @@ class ClientFormatter:
 
 
     This class is constructed with a Python standard socket address tuple.
     This class is constructed with a Python standard socket address tuple.
     If it's 2-element tuple, it's assumed to be an IPv4 socket address
     If it's 2-element tuple, it's assumed to be an IPv4 socket address
-    and will be converted to the form of '<addr>:<port>'.
+    and will be converted to the form of '<addr>:<port>(/key=<tsig-key>)'.
     If it's 4-element tuple, it's assumed to be an IPv6 socket address.
     If it's 4-element tuple, it's assumed to be an IPv6 socket address.
-    and will be converted to the form of '[<addr>]:<por>'.
+    and will be converted to the form of '[<addr>]:<por>(/key=<tsig-key>)'.
+    The optional key=<tsig-key> will be added if a TSIG record is given
+    on construction.  tsig-key is the TSIG key name in that case.
 
 
     This class is designed to delay the conversion until it's explicitly
     This class is designed to delay the conversion until it's explicitly
     requested, so the conversion doesn't happen if the corresponding log
     requested, so the conversion doesn't happen if the corresponding log
@@ -45,16 +47,23 @@ class ClientFormatter:
     Right now this is an open issue.
     Right now this is an open issue.
 
 
     """
     """
-    def __init__(self, addr):
+    def __init__(self, addr, tsig_record=None):
         self.__addr = addr
         self.__addr = addr
+        self.__tsig_record = tsig_record
 
 
-    def __str__(self):
+    def __format_addr(self):
         if len(self.__addr) == 2:
         if len(self.__addr) == 2:
             return self.__addr[0] + ':' + str(self.__addr[1])
             return self.__addr[0] + ':' + str(self.__addr[1])
         elif len(self.__addr) == 4:
         elif len(self.__addr) == 4:
             return '[' + self.__addr[0] + ']:' + str(self.__addr[1])
             return '[' + self.__addr[0] + ']:' + str(self.__addr[1])
         return None
         return None
 
 
+    def __str__(self):
+        format = self.__format_addr()
+        if format is not None and self.__tsig_record is not None:
+            format += '/key=' + self.__tsig_record.get_name().to_text(True)
+        return format
+
 class ZoneFormatter:
 class ZoneFormatter:
     """A utility class to convert zone name and class to string.
     """A utility class to convert zone name and class to string.
 
 

+ 45 - 22
src/lib/python/isc/ddns/session.py

@@ -21,6 +21,7 @@ from isc.ddns.logger import logger, ClientFormatter, ZoneFormatter,\
 from isc.log_messages.libddns_messages import *
 from isc.log_messages.libddns_messages import *
 from isc.datasrc import ZoneFinder
 from isc.datasrc import ZoneFinder
 import isc.xfrin.diff
 import isc.xfrin.diff
+from isc.acl.acl import ACCEPT, REJECT, DROP
 import copy
 import copy
 
 
 # Result codes for UpdateSession.handle()
 # Result codes for UpdateSession.handle()
@@ -48,7 +49,8 @@ class UpdateError(Exception):
     - msg (string) A string explaining the error.
     - msg (string) A string explaining the error.
     - zname (isc.dns.Name) The zone name.  Can be None when not identified.
     - zname (isc.dns.Name) The zone name.  Can be None when not identified.
     - zclass (isc.dns.RRClass) The zone class.  Like zname, can be None.
     - zclass (isc.dns.RRClass) The zone class.  Like zname, can be None.
-    - rcode (isc.dns.RCode) The RCODE to be set in the response message.
+    - rcode (isc.dns.RCode or None) The RCODE to be set in the response
+      message; this can be None if the response is not expected to be sent.
     - nolog (bool) If True, it indicates there's no more need for logging.
     - nolog (bool) If True, it indicates there's no more need for logging.
 
 
     '''
     '''
@@ -133,30 +135,24 @@ class UpdateSession:
     class can use the message to send a response to the client.
     class can use the message to send a response to the client.
 
 
     '''
     '''
-    def __init__(self, req_message, req_data, client_addr, zone_config):
+    def __init__(self, req_message, client_addr, zone_config):
         '''Constructor.
         '''Constructor.
 
 
-        Note: req_data is not really used as of #1512 but is listed since
-        it's quite likely we need it in a subsequent task soon.  We'll
-        also need to get other parameters such as ACLs, for which, it's less
-        clear in which form we want to get the information, so it's left
-        open for now.
-
         Parameters:
         Parameters:
         - req_message (isc.dns.Message) The request message.  This must be
         - req_message (isc.dns.Message) The request message.  This must be
-          in the PARSE mode.
-        - req_data (binary) Wire format data of the request message.
-          It will be used for TSIG verification if necessary.
+          in the PARSE mode, its Opcode must be UPDATE, and must have been
+          TSIG validatd if it's TSIG signed.
         - client_addr (socket address) The address/port of the update client
         - client_addr (socket address) The address/port of the update client
           in the form of Python socket address object.  This is mainly for
           in the form of Python socket address object.  This is mainly for
           logging and access control.
           logging and access control.
         - zone_config (ZoneConfig) A tentative container that encapsulates
         - zone_config (ZoneConfig) A tentative container that encapsulates
           the server's zone configuration.  See zone_config.py.
           the server's zone configuration.  See zone_config.py.
-
-        (It'll soon need to be passed ACL in some way, too)
+        - req_data (binary) Wire format data of the request message.
+          It will be used for TSIG verification if necessary.
 
 
         '''
         '''
         self.__message = req_message
         self.__message = req_message
+        self.__tsig = req_message.get_tsig_record()
         self.__client_addr = client_addr
         self.__client_addr = client_addr
         self.__zone_config = zone_config
         self.__zone_config = zone_config
         self.__added_soa = None
         self.__added_soa = None
@@ -165,8 +161,10 @@ class UpdateSession:
         '''Return the update message.
         '''Return the update message.
 
 
         After handle() is called, it's generally transformed to the response
         After handle() is called, it's generally transformed to the response
-        to be returned to the client; otherwise it would be identical to
-        the request message passed on construction.
+        to be returned to the client.  If the request has been dropped,
+        this method returns None.  If this method is called before handle()
+        the return value would be identical to the request message passed on
+        construction, although it's of no practical use.
 
 
         '''
         '''
         return self.__message
         return self.__message
@@ -182,7 +180,8 @@ class UpdateSession:
           UPDATE_DROP Error happened and no response should be sent.
           UPDATE_DROP Error happened and no response should be sent.
           Except the case of UPDATE_DROP, the UpdateSession object will have
           Except the case of UPDATE_DROP, the UpdateSession object will have
           created a response that is to be returned to the request client,
           created a response that is to be returned to the request client,
-          which can be retrieved by get_message().
+          which can be retrieved by get_message().  If it's UPDATE_DROP,
+          subsequent call to get_message() returns None.
         - The name of the updated zone (isc.dns.Name object) in case of
         - The name of the updated zone (isc.dns.Name object) in case of
           UPDATE_SUCCESS; otherwise None.
           UPDATE_SUCCESS; otherwise None.
         - The RR class of the updated zone (isc.dns.RRClass object) in case
         - The RR class of the updated zone (isc.dns.RRClass object) in case
@@ -196,7 +195,7 @@ class UpdateSession:
             if prereq_result != Rcode.NOERROR():
             if prereq_result != Rcode.NOERROR():
                 self.__make_response(prereq_result)
                 self.__make_response(prereq_result)
                 return UPDATE_ERROR, self.__zname, self.__zclass
                 return UPDATE_ERROR, self.__zname, self.__zclass
-            # self.__check_update_acl()
+            self.__check_update_acl(self.__zname, self.__zclass)
             update_result = self.__do_update()
             update_result = self.__do_update()
             if update_result != Rcode.NOERROR():
             if update_result != Rcode.NOERROR():
                 self.__make_response(update_result)
                 self.__make_response(update_result)
@@ -206,10 +205,15 @@ class UpdateSession:
         except UpdateError as e:
         except UpdateError as e:
             if not e.nolog:
             if not e.nolog:
                 logger.debug(logger.DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_ERROR,
                 logger.debug(logger.DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_ERROR,
-                             ClientFormatter(self.__client_addr),
+                             ClientFormatter(self.__client_addr, self.__tsig),
                              ZoneFormatter(e.zname, e.zclass), e)
                              ZoneFormatter(e.zname, e.zclass), e)
-            self.__make_response(e.rcode)
-            return UPDATE_ERROR, None, None
+            # If RCODE is specified, create a corresponding resonse and return
+            # ERROR; otherwise clear the message and return DROP.
+            if e.rcode is not None:
+                self.__make_response(e.rcode)
+                return UPDATE_ERROR, None, None
+            self.__message = None
+            return UPDATE_DROP, None, None
 
 
     def __get_update_zone(self):
     def __get_update_zone(self):
         '''Parse the zone section and find the zone to be updated.
         '''Parse the zone section and find the zone to be updated.
@@ -248,15 +252,34 @@ class UpdateSession:
             # We are a secondary server; since we don't yet support update
             # We are a secondary server; since we don't yet support update
             # forwarding, we return 'not implemented'.
             # forwarding, we return 'not implemented'.
             logger.debug(DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_FORWARD_FAIL,
             logger.debug(DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_FORWARD_FAIL,
-                         ClientFormatter(self.__client_addr),
+                         ClientFormatter(self.__client_addr, self.__tsig),
                          ZoneFormatter(zname, zclass))
                          ZoneFormatter(zname, zclass))
             raise UpdateError('forward', zname, zclass, Rcode.NOTIMP(), True)
             raise UpdateError('forward', zname, zclass, Rcode.NOTIMP(), True)
         # zone wasn't found
         # zone wasn't found
         logger.debug(DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_NOTAUTH,
         logger.debug(DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_NOTAUTH,
-                     ClientFormatter(self.__client_addr),
+                     ClientFormatter(self.__client_addr, self.__tsig),
                      ZoneFormatter(zname, zclass))
                      ZoneFormatter(zname, zclass))
         raise UpdateError('notauth', zname, zclass, Rcode.NOTAUTH(), True)
         raise UpdateError('notauth', zname, zclass, Rcode.NOTAUTH(), True)
 
 
+    def __check_update_acl(self, zname, zclass):
+        '''Apply update ACL for the zone to be updated.'''
+        acl = self.__zone_config.get_update_acl(zname, zclass)
+        action = acl.execute(isc.acl.dns.RequestContext(
+                (self.__client_addr[0], self.__client_addr[1]), self.__tsig))
+        if action == REJECT:
+            logger.info(LIBDDNS_UPDATE_DENIED,
+                        ClientFormatter(self.__client_addr, self.__tsig),
+                        ZoneFormatter(zname, zclass))
+            raise UpdateError('rejected', zname, zclass, Rcode.REFUSED(), True)
+        if action == DROP:
+            logger.info(LIBDDNS_UPDATE_DROPPED,
+                        ClientFormatter(self.__client_addr, self.__tsig),
+                        ZoneFormatter(zname, zclass))
+            raise UpdateError('dropped', zname, zclass, None, True)
+        logger.debug(logger.DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_APPROVED,
+                     ClientFormatter(self.__client_addr, self.__tsig),
+                     ZoneFormatter(zname, zclass))
+
     def __make_response(self, rcode):
     def __make_response(self, rcode):
         '''Transform the internal message to the update response.
         '''Transform the internal message to the update response.
 
 

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

@@ -6,7 +6,7 @@ CLEANFILES = $(builddir)/rwtest.sqlite3.copied
 # If necessary (rare cases), explicitly specify paths to dynamic libraries
 # If necessary (rare cases), explicitly specify paths to dynamic libraries
 # required by loadable python modules.
 # required by loadable python modules.
 if SET_ENV_LIBRARY_PATH
 if SET_ENV_LIBRARY_PATH
-LIBRARY_PATH_PLACEHOLDER = $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
+LIBRARY_PATH_PLACEHOLDER = $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/acl/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
 endif
 endif
 
 
 # test using command-line arguments, so use check-local target instead of TESTS
 # test using command-line arguments, so use check-local target instead of TESTS

+ 121 - 60
src/lib/python/isc/ddns/tests/session_tests.py

@@ -35,9 +35,11 @@ TEST_RRCLASS = RRClass.IN()
 TEST_ZONE_RECORD = Question(TEST_ZONE_NAME, TEST_RRCLASS, UPDATE_RRTYPE)
 TEST_ZONE_RECORD = Question(TEST_ZONE_NAME, TEST_RRCLASS, UPDATE_RRTYPE)
 TEST_CLIENT6 = ('2001:db8::1', 53, 0, 0)
 TEST_CLIENT6 = ('2001:db8::1', 53, 0, 0)
 TEST_CLIENT4 = ('192.0.2.1', 53)
 TEST_CLIENT4 = ('192.0.2.1', 53)
+# TSIG key for tests when needed.  The key name is TEST_ZONE_NAME.
+TEST_TSIG_KEY = TSIGKey("example.org:SFuWd/q99SzF8Yzd1QbB9g==")
 
 
 def create_update_msg(zones=[TEST_ZONE_RECORD], prerequisites=[],
 def create_update_msg(zones=[TEST_ZONE_RECORD], prerequisites=[],
-                      updates=[]):
+                      updates=[], tsig_key=None):
     msg = Message(Message.RENDER)
     msg = Message(Message.RENDER)
     msg.set_qid(5353)           # arbitrary chosen
     msg.set_qid(5353)           # arbitrary chosen
     msg.set_opcode(Opcode.UPDATE())
     msg.set_opcode(Opcode.UPDATE())
@@ -50,13 +52,16 @@ def create_update_msg(zones=[TEST_ZONE_RECORD], prerequisites=[],
         msg.add_rrset(SECTION_UPDATE, u)
         msg.add_rrset(SECTION_UPDATE, u)
 
 
     renderer = MessageRenderer()
     renderer = MessageRenderer()
-    msg.to_wire(renderer)
+    if tsig_key is not None:
+        msg.to_wire(renderer, TSIGContext(tsig_key))
+    else:
+        msg.to_wire(renderer)
 
 
     # re-read the created data in the parse mode
     # re-read the created data in the parse mode
     msg.clear(Message.PARSE)
     msg.clear(Message.PARSE)
     msg.from_wire(renderer.get_data(), Message.PRESERVE_ORDER)
     msg.from_wire(renderer.get_data(), Message.PRESERVE_ORDER)
 
 
-    return renderer.get_data(), msg
+    return msg
 
 
 def add_rdata(rrset, rdata):
 def add_rdata(rrset, rdata):
     '''
     '''
@@ -89,18 +94,25 @@ def create_rrset(name, rrclass, rrtype, ttl, rdatas = []):
         add_rdata(rrset, rdata)
         add_rdata(rrset, rdata)
     return rrset
     return rrset
 
 
-class SessionTest(unittest.TestCase):
-    '''Session tests'''
+class SesseionTestBase(unittest.TestCase):
+    '''Base class for all sesion related tests.
+
+    It just initializes common test parameters in its setUp() and defines
+    some common utility method(s).
+
+    '''
     def setUp(self):
     def setUp(self):
         shutil.copyfile(READ_ZONE_DB_FILE, WRITE_ZONE_DB_FILE)
         shutil.copyfile(READ_ZONE_DB_FILE, WRITE_ZONE_DB_FILE)
-        self.__datasrc_client = DataSourceClient("sqlite3",
-                                                 WRITE_ZONE_DB_CONFIG)
-        self.__update_msgdata, self.__update_msg = create_update_msg()
-        self.__session = UpdateSession(self.__update_msg,
-                                       self.__update_msgdata, TEST_CLIENT4,
-                                       ZoneConfig([], TEST_RRCLASS,
-                                                  self.__datasrc_client))
-        self.__session._UpdateSession__get_update_zone()
+        self._datasrc_client = DataSourceClient("sqlite3",
+                                                WRITE_ZONE_DB_CONFIG)
+        self._update_msg = create_update_msg()
+        self._acl_map = {(TEST_ZONE_NAME, TEST_RRCLASS):
+                             REQUEST_LOADER.load([{"action": "ACCEPT"}])}
+        self._session = UpdateSession(self._update_msg, TEST_CLIENT4,
+                                      ZoneConfig([], TEST_RRCLASS,
+                                                 self._datasrc_client,
+                                                 self._acl_map))
+        self._session._UpdateSession__get_update_zone()
 
 
     def check_response(self, msg, expected_rcode):
     def check_response(self, msg, expected_rcode):
         '''Perform common checks on update resposne message.'''
         '''Perform common checks on update resposne message.'''
@@ -114,9 +126,12 @@ class SessionTest(unittest.TestCase):
         self.assertEqual(0, msg.get_rr_count(SECTION_UPDATE))
         self.assertEqual(0, msg.get_rr_count(SECTION_UPDATE))
         self.assertEqual(0, msg.get_rr_count(Message.SECTION_ADDITIONAL))
         self.assertEqual(0, msg.get_rr_count(Message.SECTION_ADDITIONAL))
 
 
+class SessionTest(SesseionTestBase):
+    '''Basic session tests'''
+
     def test_handle(self):
     def test_handle(self):
         '''Basic update case'''
         '''Basic update case'''
-        result, zname, zclass = self.__session.handle()
+        result, zname, zclass = self._session.handle()
         self.assertEqual(UPDATE_SUCCESS, result)
         self.assertEqual(UPDATE_SUCCESS, result)
         self.assertEqual(TEST_ZONE_NAME, zname)
         self.assertEqual(TEST_ZONE_NAME, zname)
         self.assertEqual(TEST_RRCLASS, zclass)
         self.assertEqual(TEST_RRCLASS, zclass)
@@ -127,8 +142,8 @@ class SessionTest(unittest.TestCase):
 
 
     def test_broken_request(self):
     def test_broken_request(self):
         # Zone section is empty
         # Zone section is empty
-        msg_data, msg = create_update_msg(zones=[])
-        session = UpdateSession(msg, msg_data, TEST_CLIENT6, None)
+        msg = create_update_msg(zones=[])
+        session = UpdateSession(msg, TEST_CLIENT6, None)
         result, zname, zclass = session.handle()
         result, zname, zclass = session.handle()
         self.assertEqual(UPDATE_ERROR, result)
         self.assertEqual(UPDATE_ERROR, result)
         self.assertEqual(None, zname)
         self.assertEqual(None, zname)
@@ -136,17 +151,15 @@ class SessionTest(unittest.TestCase):
         self.check_response(session.get_message(), Rcode.FORMERR())
         self.check_response(session.get_message(), Rcode.FORMERR())
 
 
         # Zone section contains multiple records
         # Zone section contains multiple records
-        msg_data, msg = create_update_msg(zones=[TEST_ZONE_RECORD,
-                                                 TEST_ZONE_RECORD])
-        session = UpdateSession(msg, msg_data, TEST_CLIENT4, None)
+        msg = create_update_msg(zones=[TEST_ZONE_RECORD, TEST_ZONE_RECORD])
+        session = UpdateSession(msg, TEST_CLIENT4, None)
         self.assertEqual(UPDATE_ERROR, session.handle()[0])
         self.assertEqual(UPDATE_ERROR, session.handle()[0])
         self.check_response(session.get_message(), Rcode.FORMERR())
         self.check_response(session.get_message(), Rcode.FORMERR())
 
 
         # Zone section's type is not SOA
         # Zone section's type is not SOA
-        msg_data, msg = create_update_msg(zones=[Question(TEST_ZONE_NAME,
-                                                          TEST_RRCLASS,
-                                                          RRType.A())])
-        session = UpdateSession(msg, msg_data, TEST_CLIENT4, None)
+        msg = create_update_msg(zones=[Question(TEST_ZONE_NAME, TEST_RRCLASS,
+                                                RRType.A())])
+        session = UpdateSession(msg, TEST_CLIENT4, None)
         self.assertEqual(UPDATE_ERROR, session.handle()[0])
         self.assertEqual(UPDATE_ERROR, session.handle()[0])
         self.check_response(session.get_message(), Rcode.FORMERR())
         self.check_response(session.get_message(), Rcode.FORMERR())
 
 
@@ -154,24 +167,20 @@ class SessionTest(unittest.TestCase):
         # specified zone is configured as a secondary.  Since this
         # specified zone is configured as a secondary.  Since this
         # implementation doesn't support update forwarding, the result
         # implementation doesn't support update forwarding, the result
         # should be NOTIMP.
         # should be NOTIMP.
-        msg_data, msg = create_update_msg(zones=[Question(TEST_ZONE_NAME,
-                                                          TEST_RRCLASS,
-                                                          RRType.SOA())])
-        session = UpdateSession(msg, msg_data, TEST_CLIENT4,
+        msg = create_update_msg(zones=[Question(TEST_ZONE_NAME, TEST_RRCLASS,
+                                                RRType.SOA())])
+        session = UpdateSession(msg, TEST_CLIENT4,
                                 ZoneConfig([(TEST_ZONE_NAME, TEST_RRCLASS)],
                                 ZoneConfig([(TEST_ZONE_NAME, TEST_RRCLASS)],
-                                           TEST_RRCLASS,
-                                           self.__datasrc_client))
+                                           TEST_RRCLASS, self._datasrc_client))
         self.assertEqual(UPDATE_ERROR, session.handle()[0])
         self.assertEqual(UPDATE_ERROR, session.handle()[0])
         self.check_response(session.get_message(), Rcode.NOTIMP())
         self.check_response(session.get_message(), Rcode.NOTIMP())
 
 
     def check_notauth(self, zname, zclass=TEST_RRCLASS):
     def check_notauth(self, zname, zclass=TEST_RRCLASS):
         '''Common test sequence for the 'notauth' test'''
         '''Common test sequence for the 'notauth' test'''
-        msg_data, msg = create_update_msg(zones=[Question(zname, zclass,
-                                                          RRType.SOA())])
-        session = UpdateSession(msg, msg_data, TEST_CLIENT4,
+        msg = create_update_msg(zones=[Question(zname, zclass, RRType.SOA())])
+        session = UpdateSession(msg, TEST_CLIENT4,
                                 ZoneConfig([(TEST_ZONE_NAME, TEST_RRCLASS)],
                                 ZoneConfig([(TEST_ZONE_NAME, TEST_RRCLASS)],
-                                           TEST_RRCLASS,
-                                           self.__datasrc_client))
+                                           TEST_RRCLASS, self._datasrc_client))
         self.assertEqual(UPDATE_ERROR, session.handle()[0])
         self.assertEqual(UPDATE_ERROR, session.handle()[0])
         self.check_response(session.get_message(), Rcode.NOTAUTH())
         self.check_response(session.get_message(), Rcode.NOTAUTH())
 
 
@@ -274,7 +283,7 @@ class SessionTest(unittest.TestCase):
         self.assertEqual(expected, strings)
         self.assertEqual(expected, strings)
 
 
     def __prereq_helper(self, method, expected, rrset):
     def __prereq_helper(self, method, expected, rrset):
-        '''Calls the given method with self.__datasrc_client
+        '''Calls the given method with self._datasrc_client
            and the given rrset, and compares the return value.
            and the given rrset, and compares the return value.
            Function does not do much but makes the code look nicer'''
            Function does not do much but makes the code look nicer'''
         self.assertEqual(expected, method(rrset))
         self.assertEqual(expected, method(rrset))
@@ -331,19 +340,19 @@ class SessionTest(unittest.TestCase):
         self.__prereq_helper(method, expected, rrset)
         self.__prereq_helper(method, expected, rrset)
 
 
     def test_check_prerequisite_exists(self):
     def test_check_prerequisite_exists(self):
-        method = self.__session._UpdateSession__prereq_rrset_exists
+        method = self._session._UpdateSession__prereq_rrset_exists
         self.__check_prerequisite_exists_combined(method,
         self.__check_prerequisite_exists_combined(method,
                                                   RRClass.ANY(),
                                                   RRClass.ANY(),
                                                   True)
                                                   True)
 
 
     def test_check_prerequisite_does_not_exist(self):
     def test_check_prerequisite_does_not_exist(self):
-        method = self.__session._UpdateSession__prereq_rrset_does_not_exist
+        method = self._session._UpdateSession__prereq_rrset_does_not_exist
         self.__check_prerequisite_exists_combined(method,
         self.__check_prerequisite_exists_combined(method,
                                                   RRClass.NONE(),
                                                   RRClass.NONE(),
                                                   False)
                                                   False)
 
 
     def test_check_prerequisite_exists_value(self):
     def test_check_prerequisite_exists_value(self):
-        method = self.__session._UpdateSession__prereq_rrset_exists_value
+        method = self._session._UpdateSession__prereq_rrset_exists_value
 
 
         rrset = create_rrset("www.example.org", RRClass.IN(), RRType.A(), 0)
         rrset = create_rrset("www.example.org", RRClass.IN(), RRType.A(), 0)
         # empty one should not match
         # empty one should not match
@@ -419,13 +428,13 @@ class SessionTest(unittest.TestCase):
         self.__prereq_helper(method, expected, rrset)
         self.__prereq_helper(method, expected, rrset)
 
 
     def test_check_prerequisite_name_in_use(self):
     def test_check_prerequisite_name_in_use(self):
-        method = self.__session._UpdateSession__prereq_name_in_use
+        method = self._session._UpdateSession__prereq_name_in_use
         self.__check_prerequisite_name_in_use_combined(method,
         self.__check_prerequisite_name_in_use_combined(method,
                                                        RRClass.ANY(),
                                                        RRClass.ANY(),
                                                        True)
                                                        True)
 
 
     def test_check_prerequisite_name_not_in_use(self):
     def test_check_prerequisite_name_not_in_use(self):
-        method = self.__session._UpdateSession__prereq_name_not_in_use
+        method = self._session._UpdateSession__prereq_name_not_in_use
         self.__check_prerequisite_name_in_use_combined(method,
         self.__check_prerequisite_name_in_use_combined(method,
                                                        RRClass.NONE(),
                                                        RRClass.NONE(),
                                                        False)
                                                        False)
@@ -435,10 +444,10 @@ class SessionTest(unittest.TestCase):
            creates an update session, and fills it with the list of rrsets
            creates an update session, and fills it with the list of rrsets
            from 'prerequisites'. Then checks if __check_prerequisites()
            from 'prerequisites'. Then checks if __check_prerequisites()
            returns the Rcode specified in 'expected'.'''
            returns the Rcode specified in 'expected'.'''
-        msg_data, msg = create_update_msg([TEST_ZONE_RECORD],
-                                          prerequisites)
-        zconfig = ZoneConfig([], TEST_RRCLASS, self.__datasrc_client)
-        session = UpdateSession(msg, msg_data, TEST_CLIENT4, zconfig)
+        msg = create_update_msg([TEST_ZONE_RECORD], prerequisites)
+        zconfig = ZoneConfig([], TEST_RRCLASS, self._datasrc_client,
+                             self._acl_map)
+        session = UpdateSession(msg, TEST_CLIENT4, zconfig)
         session._UpdateSession__get_update_zone()
         session._UpdateSession__get_update_zone()
         # compare the to_text output of the rcodes (nicer error messages)
         # compare the to_text output of the rcodes (nicer error messages)
         # This call itself should also be done by handle(),
         # This call itself should also be done by handle(),
@@ -447,8 +456,8 @@ class SessionTest(unittest.TestCase):
             session._UpdateSession__check_prerequisites().to_text())
             session._UpdateSession__check_prerequisites().to_text())
         # Now see if handle finds the same result
         # Now see if handle finds the same result
         (result, _, _) = session.handle()
         (result, _, _) = session.handle()
-        self.assertEqual(expected,
-                         session._UpdateSession__message.get_rcode())
+        self.assertEqual(expected.to_text(),
+                         session._UpdateSession__message.get_rcode().to_text())
         # And that the result looks right
         # And that the result looks right
         if expected == Rcode.NOERROR():
         if expected == Rcode.NOERROR():
             self.assertEqual(UPDATE_SUCCESS, result)
             self.assertEqual(UPDATE_SUCCESS, result)
@@ -460,10 +469,10 @@ class SessionTest(unittest.TestCase):
            creates an update session, and fills it with the list of rrsets
            creates an update session, and fills it with the list of rrsets
            from 'updates'. Then checks if __do_prescan()
            from 'updates'. Then checks if __do_prescan()
            returns the Rcode specified in 'expected'.'''
            returns the Rcode specified in 'expected'.'''
-        msg_data, msg = create_update_msg([TEST_ZONE_RECORD],
-                                          [], updates)
-        zconfig = ZoneConfig([], TEST_RRCLASS, self.__datasrc_client)
-        session = UpdateSession(msg, msg_data, TEST_CLIENT4, zconfig)
+        msg = create_update_msg([TEST_ZONE_RECORD], [], updates)
+        zconfig = ZoneConfig([], TEST_RRCLASS, self._datasrc_client,
+                             self._acl_map)
+        session = UpdateSession(msg, TEST_CLIENT4, zconfig)
         session._UpdateSession__get_update_zone()
         session._UpdateSession__get_update_zone()
         # compare the to_text output of the rcodes (nicer error messages)
         # compare the to_text output of the rcodes (nicer error messages)
         # This call itself should also be done by handle(),
         # This call itself should also be done by handle(),
@@ -479,10 +488,10 @@ class SessionTest(unittest.TestCase):
            creates an update session, and fills it with the list of rrsets
            creates an update session, and fills it with the list of rrsets
            from 'updates'. Then checks if __handle()
            from 'updates'. Then checks if __handle()
            results in a response with rcode 'expected'.'''
            results in a response with rcode 'expected'.'''
-        msg_data, msg = create_update_msg([TEST_ZONE_RECORD],
-                                          [], updates)
-        zconfig = ZoneConfig([], TEST_RRCLASS, self.__datasrc_client)
-        session = UpdateSession(msg, msg_data, TEST_CLIENT4, zconfig)
+        msg = create_update_msg([TEST_ZONE_RECORD], [], updates)
+        zconfig = ZoneConfig([], TEST_RRCLASS, self._datasrc_client,
+                             self._acl_map)
+        session = UpdateSession(msg, TEST_CLIENT4, zconfig)
 
 
         # Now see if handle finds the same result
         # Now see if handle finds the same result
         (result, _, _) = session.handle()
         (result, _, _) = session.handle()
@@ -544,7 +553,6 @@ class SessionTest(unittest.TestCase):
 
 
         name_not_in_use_no = create_rrset("www.example.org", RRClass.NONE(),
         name_not_in_use_no = create_rrset("www.example.org", RRClass.NONE(),
                                           RRType.ANY(), 0)
                                           RRType.ANY(), 0)
-
         # check 'no' result codes
         # check 'no' result codes
         self.check_prerequisite_result(Rcode.NXRRSET(),
         self.check_prerequisite_result(Rcode.NXRRSET(),
                                        [ rrset_exists_no ])
                                        [ rrset_exists_no ])
@@ -639,9 +647,8 @@ class SessionTest(unittest.TestCase):
                              [ "foo" ])
                              [ "foo" ])
         self.check_prerequisite_result(Rcode.FORMERR(), [ rrset ])
         self.check_prerequisite_result(Rcode.FORMERR(), [ rrset ])
 
 
-
     def __prereq_helper(self, method, expected, rrset):
     def __prereq_helper(self, method, expected, rrset):
-        '''Calls the given method with self.__datasrc_client
+        '''Calls the given method with self._datasrc_client
            and the given rrset, and compares the return value.
            and the given rrset, and compares the return value.
            Function does not do much but makes the code look nicer'''
            Function does not do much but makes the code look nicer'''
         self.assertEqual(expected, method(rrset))
         self.assertEqual(expected, method(rrset))
@@ -828,7 +835,7 @@ class SessionTest(unittest.TestCase):
            then checks if the result matches the expected result.
            then checks if the result matches the expected result.
            If so, and if expected_rrset is given, they are compared as
            If so, and if expected_rrset is given, they are compared as
            well.'''
            well.'''
-        _, finder = self.__datasrc_client.find_zone(TEST_ZONE_NAME)
+        _, finder = self._datasrc_client.find_zone(TEST_ZONE_NAME)
         result, found_rrset, _ = finder.find(name, rrtype,
         result, found_rrset, _ = finder.find(name, rrtype,
                                              finder.NO_WILDCARD |
                                              finder.NO_WILDCARD |
                                              finder.FIND_GLUE_OK)
                                              finder.FIND_GLUE_OK)
@@ -1202,9 +1209,63 @@ class SessionTest(unittest.TestCase):
     def test_uncaught_exception(self):
     def test_uncaught_exception(self):
         def my_exc():
         def my_exc():
             raise Exception("foo")
             raise Exception("foo")
-        self.__session._UpdateSession__update_soa = my_exc
+        self._session._UpdateSession__update_soa = my_exc
         self.assertEqual(Rcode.SERVFAIL().to_text(),
         self.assertEqual(Rcode.SERVFAIL().to_text(),
-                         self.__session._UpdateSession__do_update().to_text())
+                         self._session._UpdateSession__do_update().to_text())
+
+class SessionACLTest(SesseionTestBase):
+    '''ACL related tests for update session.'''
+    def test_update_acl_check(self):
+        '''Test for various ACL checks.
+
+        Note that accepted cases are covered in the basic tests.
+
+        '''
+        # create a separate session, with default (empty) ACL map.
+        session = UpdateSession(self._update_msg,
+                                TEST_CLIENT4, ZoneConfig([], TEST_RRCLASS,
+                                                         self._datasrc_client))
+        # then the request should be rejected.
+        self.assertEqual((UPDATE_ERROR, None, None), session.handle())
+
+        # recreate the request message, and test with an ACL that would result
+        # in 'DROP'.  get_message() should return None.
+        msg = create_update_msg()
+        acl_map = {(TEST_ZONE_NAME, TEST_RRCLASS):
+                       REQUEST_LOADER.load([{"action": "DROP", "from":
+                                                 TEST_CLIENT4[0]}])}
+        session = UpdateSession(msg, TEST_CLIENT4,
+                                ZoneConfig([], TEST_RRCLASS,
+                                           self._datasrc_client, acl_map))
+        self.assertEqual((UPDATE_DROP, None, None), session.handle())
+        self.assertEqual(None, session.get_message())
+
+    def test_update_tsigacl_check(self):
+        '''Test for various ACL checks using TSIG.'''
+        # This ACL will accept requests from TEST_CLIENT4 (any port) *and*
+        # has TSIG signed by TEST_ZONE_NAME; all others will be rejected.
+        acl_map = {(TEST_ZONE_NAME, TEST_RRCLASS):
+                       REQUEST_LOADER.load([{"action": "ACCEPT",
+                                             "from": TEST_CLIENT4[0],
+                                             "key": TEST_ZONE_NAME.to_text()}])}
+
+        # If the message doesn't contain TSIG, it doesn't match the ACCEPT
+        # ACL entry, and the request should be rejected.
+        session = UpdateSession(self._update_msg,
+                                TEST_CLIENT4, ZoneConfig([], TEST_RRCLASS,
+                                                         self._datasrc_client,
+                                                         acl_map))
+        self.assertEqual((UPDATE_ERROR, None, None), session.handle())
+        self.check_response(session.get_message(), Rcode.REFUSED())
+
+        # If the message contains TSIG, it should match the ACCEPT
+        # ACL entry, and the request should be granted.
+        session = UpdateSession(create_update_msg(tsig_key=TEST_TSIG_KEY),
+                                TEST_CLIENT4, ZoneConfig([], TEST_RRCLASS,
+                                                         self._datasrc_client,
+                                                         acl_map))
+        self.assertEqual((UPDATE_SUCCESS, TEST_ZONE_NAME, TEST_RRCLASS),
+                         session.handle())
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
     isc.log.init("bind10")
     isc.log.init("bind10")

+ 53 - 4
src/lib/python/isc/ddns/tests/zone_config_tests.py

@@ -14,15 +14,23 @@
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
 
 import isc.log
 import isc.log
-import unittest
 from isc.dns import *
 from isc.dns import *
 from isc.datasrc import DataSourceClient
 from isc.datasrc import DataSourceClient
 from isc.ddns.zone_config import *
 from isc.ddns.zone_config import *
+import isc.acl.dns
+from isc.acl.acl import ACCEPT, REJECT, DROP, LoaderError
+
+import unittest
+import socket
 
 
 # Some common test parameters
 # Some common test parameters
 TEST_ZONE_NAME = Name('example.org')
 TEST_ZONE_NAME = Name('example.org')
 TEST_SECONDARY_ZONE_NAME = Name('example.com')
 TEST_SECONDARY_ZONE_NAME = Name('example.com')
 TEST_RRCLASS = RRClass.IN()
 TEST_RRCLASS = RRClass.IN()
+TEST_TSIG_KEY = TSIGKey("example.com:SFuWd/q99SzF8Yzd1QbB9g==")
+TEST_ACL_CONTEXT = isc.acl.dns.RequestContext(
+    socket.getaddrinfo("192.0.2.1", 1234, 0, socket.SOCK_DGRAM,
+                       socket.IPPROTO_UDP, socket.AI_NUMERICHOST)[0][4])
 
 
 class FakeDataSourceClient:
 class FakeDataSourceClient:
     '''Faked data source client used in the ZoneConfigTest.
     '''Faked data source client used in the ZoneConfigTest.
@@ -93,7 +101,7 @@ class ZoneConfigTest(unittest.TestCase):
         # empty secondary list doesn't cause any disruption.
         # empty secondary list doesn't cause any disruption.
         zconfig = ZoneConfig([], TEST_RRCLASS, self.__datasrc_client)
         zconfig = ZoneConfig([], TEST_RRCLASS, self.__datasrc_client)
         self.assertEqual((ZONE_PRIMARY, self.__datasrc_client),
         self.assertEqual((ZONE_PRIMARY, self.__datasrc_client),
-                         (self.zconfig.find_zone(TEST_ZONE_NAME, TEST_RRCLASS)))
+                         self.zconfig.find_zone(TEST_ZONE_NAME, TEST_RRCLASS))
         # adding some mulitle tuples, including subdomainof the test zone name,
         # adding some mulitle tuples, including subdomainof the test zone name,
         # and the same zone name but a different class
         # and the same zone name but a different class
         zconfig = ZoneConfig([(TEST_SECONDARY_ZONE_NAME, TEST_RRCLASS),
         zconfig = ZoneConfig([(TEST_SECONDARY_ZONE_NAME, TEST_RRCLASS),
@@ -102,14 +110,55 @@ class ZoneConfigTest(unittest.TestCase):
                               (TEST_ZONE_NAME, RRClass.CH())],
                               (TEST_ZONE_NAME, RRClass.CH())],
                              TEST_RRCLASS, self.__datasrc_client)
                              TEST_RRCLASS, self.__datasrc_client)
         self.assertEqual((ZONE_PRIMARY, self.__datasrc_client),
         self.assertEqual((ZONE_PRIMARY, self.__datasrc_client),
-                         (self.zconfig.find_zone(TEST_ZONE_NAME, TEST_RRCLASS)))
+                         self.zconfig.find_zone(TEST_ZONE_NAME, TEST_RRCLASS))
         # secondary zone list has a duplicate entry, which is just
         # secondary zone list has a duplicate entry, which is just
         # (effecitivey) ignored
         # (effecitivey) ignored
         zconfig = ZoneConfig([(TEST_SECONDARY_ZONE_NAME, TEST_RRCLASS),
         zconfig = ZoneConfig([(TEST_SECONDARY_ZONE_NAME, TEST_RRCLASS),
                               (TEST_SECONDARY_ZONE_NAME, TEST_RRCLASS)],
                               (TEST_SECONDARY_ZONE_NAME, TEST_RRCLASS)],
                              TEST_RRCLASS, self.__datasrc_client)
                              TEST_RRCLASS, self.__datasrc_client)
         self.assertEqual((ZONE_PRIMARY, self.__datasrc_client),
         self.assertEqual((ZONE_PRIMARY, self.__datasrc_client),
-                         (self.zconfig.find_zone(TEST_ZONE_NAME, TEST_RRCLASS)))
+                         self.zconfig.find_zone(TEST_ZONE_NAME, TEST_RRCLASS))
+
+class ACLConfigTest(unittest.TestCase):
+    def setUp(self):
+        self.__datasrc_client = FakeDataSourceClient()
+        self.__zconfig = ZoneConfig([(TEST_SECONDARY_ZONE_NAME, TEST_RRCLASS)],
+                                    TEST_RRCLASS, self.__datasrc_client)
+
+    def test_get_update_acl(self):
+        # By default, no ACL is set, and the default ACL is "reject all"
+        acl = self.__zconfig.get_update_acl(TEST_ZONE_NAME, TEST_RRCLASS)
+        self.assertEqual(REJECT, acl.execute(TEST_ACL_CONTEXT))
+
+        # Add a map entry that would match the request, and it should now be
+        # accepted.
+        acl_map = {(TEST_ZONE_NAME, TEST_RRCLASS):
+                   REQUEST_LOADER.load([{"action": "ACCEPT"}])}
+        self.__zconfig.set_update_acl_map(acl_map)
+        acl = self.__zconfig.get_update_acl(TEST_ZONE_NAME, TEST_RRCLASS)
+        self.assertEqual(ACCEPT, acl.execute(TEST_ACL_CONTEXT))
+
+        # 'All reject' ACL will still apply for any other zones
+        acl = self.__zconfig.get_update_acl(Name('example.com'), TEST_RRCLASS)
+        self.assertEqual(REJECT, acl.execute(TEST_ACL_CONTEXT))
+        acl = self.__zconfig.get_update_acl(TEST_ZONE_NAME, RRClass.CH())
+        self.assertEqual(REJECT, acl.execute(TEST_ACL_CONTEXT))
+
+        # Test with a map with a few more ACL entries.  Should be nothing
+        # special.
+        acl_map = {(Name('example.com'), TEST_RRCLASS):
+                       REQUEST_LOADER.load([{"action": "REJECT"}]),
+                   (TEST_ZONE_NAME, TEST_RRCLASS):
+                       REQUEST_LOADER.load([{"action": "ACCEPT"}]),
+                   (TEST_ZONE_NAME, RRClass.CH()):
+                       REQUEST_LOADER.load([{"action": "DROP"}])}
+        self.__zconfig.set_update_acl_map(acl_map)
+        acl = self.__zconfig.get_update_acl(TEST_ZONE_NAME, TEST_RRCLASS)
+        self.assertEqual(ACCEPT, acl.execute(TEST_ACL_CONTEXT))
+        acl = self.__zconfig.get_update_acl(Name('example.com'), TEST_RRCLASS)
+        self.assertEqual(REJECT, acl.execute(TEST_ACL_CONTEXT))
+        acl = self.__zconfig.get_update_acl(TEST_ZONE_NAME, RRClass.CH())
+        self.assertEqual(DROP, acl.execute(TEST_ACL_CONTEXT))
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
     isc.log.init("bind10")
     isc.log.init("bind10")

+ 38 - 1
src/lib/python/isc/ddns/zone_config.py

@@ -13,6 +13,7 @@
 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
 
+from isc.acl.dns import REQUEST_LOADER
 import isc.dns
 import isc.dns
 from isc.datasrc import DataSourceClient
 from isc.datasrc import DataSourceClient
 
 
@@ -33,7 +34,7 @@ class ZoneConfig:
     until the details are fixed.
     until the details are fixed.
 
 
     '''
     '''
-    def __init__(self, secondaries, datasrc_class, datasrc_client):
+    def __init__(self, secondaries, datasrc_class, datasrc_client, acl_map={}):
         '''Constructor.
         '''Constructor.
 
 
         Parameters:
         Parameters:
@@ -45,6 +46,11 @@ class ZoneConfig:
         - datasrc_client: isc.dns.DataSourceClient object.  A data source
         - datasrc_client: isc.dns.DataSourceClient object.  A data source
           class for the RR class of datasrc_class.  It's expected to contain
           class for the RR class of datasrc_class.  It's expected to contain
           a zone that is eventually updated in the ddns package.
           a zone that is eventually updated in the ddns package.
+        - acl_map: a dictionary that maps a tuple of
+          (isc.dns.Name, isc.dns.RRClass) to an isc.dns.dns.RequestACL
+          object.  It defines an ACL to be applied to the zone defined
+          by the tuple.  If unspecified, or the map is empty, the default
+          ACL will be applied to all zones, which is to reject any requests.
 
 
         '''
         '''
         self.__secondaries = set()
         self.__secondaries = set()
@@ -52,6 +58,8 @@ class ZoneConfig:
             self.__secondaries.add((zname, zclass))
             self.__secondaries.add((zname, zclass))
         self.__datasrc_class = datasrc_class
         self.__datasrc_class = datasrc_class
         self.__datasrc_client = datasrc_client
         self.__datasrc_client = datasrc_client
+        self.__default_acl = REQUEST_LOADER.load([{"action": "REJECT"}])
+        self.__acl_map = acl_map
 
 
     def find_zone(self, zone_name, zone_class):
     def find_zone(self, zone_name, zone_class):
         '''Return the type and accessor client object for given zone.'''
         '''Return the type and accessor client object for given zone.'''
@@ -62,3 +70,32 @@ class ZoneConfig:
                 return ZONE_SECONDARY, None
                 return ZONE_SECONDARY, None
             return ZONE_PRIMARY, self.__datasrc_client
             return ZONE_PRIMARY, self.__datasrc_client
         return ZONE_NOTFOUND, None
         return ZONE_NOTFOUND, None
+
+    def get_update_acl(self, zone_name, zone_class):
+        '''Return the update ACL for the given zone.
+
+        This method searches the internally stored ACL map to see if
+        there's an ACL to be applied to the given zone.  If found, that
+        ACL will be returned; otherwise the default ACL (see the constructor
+        description) will be returned.
+
+        Parameters:
+        zone_name (isc.dns.Name): The zone name.
+        zone_class (isc.dns.RRClass): The zone class.
+        '''
+        acl = self.__acl_map.get((zone_name, zone_class))
+        if acl is not None:
+            return acl
+        return self.__default_acl
+
+    def set_update_acl_map(self, new_map):
+        '''Set a new ACL map.
+
+        This replaces any stored ACL map, either at construction or
+        by a previous call to this method, with the given new one.
+
+        Parameter:
+        new_map: same as the acl_map parameter of the constructor.
+
+        '''
+        self.__acl_map = new_map

+ 1 - 1
src/lib/util/locks.h

@@ -16,7 +16,7 @@
 /// It also contains code to use boost/threads locks:
 /// It also contains code to use boost/threads locks:
 ///
 ///
 ///
 ///
-/// All locks are dummy classes that don't actually do anything. At this moment, 
+/// All locks are dummy classes that don't actually do anything. At this moment,
 /// only the very minimal set of methods that we actually use is defined.
 /// only the very minimal set of methods that we actually use is defined.
 ///
 ///
 /// Note that we need to include <config.h> in our .cc files for that
 /// Note that we need to include <config.h> in our .cc files for that

+ 8 - 2
src/lib/xfr/xfrout_client.cc

@@ -82,8 +82,14 @@ XfroutClient::sendXfroutRequestInfo(const int tcp_sock,
 
 
     // TODO: this shouldn't be blocking send, even though it's unlikely to
     // TODO: this shouldn't be blocking send, even though it's unlikely to
     // block.
     // block.
-    // converting the 16-bit word to network byte order.
-    const uint8_t lenbuf[2] = { msg_len >> 8, msg_len & 0xff };
+    // Converting the 16-bit word to network byte order.
+
+    // Splitting msg_len below performs something called a 'narrowing
+    // conversion' (conversion of uint16_t to uint8_t). C++0x (and GCC
+    // 4.7) requires explicit casting when a narrowing conversion is
+    // performed. For reference, see 8.5.4/6 of n3225.
+    const uint8_t lenbuf[2] = { static_cast<uint8_t>(msg_len >> 8),
+                                static_cast<uint8_t>(msg_len & 0xff) };
     if (send(impl_->socket_.native(), lenbuf, sizeof(lenbuf), 0) !=
     if (send(impl_->socket_.native(), lenbuf, sizeof(lenbuf), 0) !=
         sizeof(lenbuf)) {
         sizeof(lenbuf)) {
         isc_throw(XfroutError,
         isc_throw(XfroutError,

+ 1 - 1
tests/lettuce/features/bindctl_commands.feature

@@ -109,7 +109,7 @@ Feature: control with bindctl
         # nested_command contains another execute script
         # nested_command contains another execute script
         When I send bind10 the command execute file data/commands/nested
         When I send bind10 the command execute file data/commands/nested
         last bindctl output should contain shouldshow
         last bindctl output should contain shouldshow
-        last bindctl output should not contain Error    
+        last bindctl output should not contain Error
 
 
         # show commands from a file
         # show commands from a file
         When I send bind10 the command execute file data/commands/bad_command show
         When I send bind10 the command execute file data/commands/bad_command show

+ 6 - 13
tests/tools/perfdhcp/Makefile.am

@@ -12,19 +12,12 @@ if USE_STATIC_LINK
 AM_LDFLAGS += -static
 AM_LDFLAGS += -static
 endif
 endif
 
 
-# We have to suppress warnings because we are compiling C code with CXX
-# We have to do this to link with new C++ pieces of code
-perfdhcp_CXXFLAGS = $(AM_CXXFLAGS)
-if USE_CLANGPP
-perfdhcp_CXXFLAGS += -Wno-error
-else
-if USE_GXX
-perfdhcp_CXXFLAGS += -Wno-write-strings
-endif
-endif
+lib_LTLIBRARIES = libperfdhcp++.la
+libperfdhcp___la_SOURCES = command_options.cc command_options.h
+libperfdhcp___la_CXXFLAGS = $(AM_CXXFLAGS)
+libperfdhcp___la_LIBADD = $(top_builddir)/src/lib/exceptions/libexceptions.la
 
 
 pkglibexec_PROGRAMS  = perfdhcp
 pkglibexec_PROGRAMS  = perfdhcp
-perfdhcp_SOURCES  = perfdhcp.cc
-perfdhcp_SOURCES += command_options.cc command_options.h
+perfdhcp_SOURCES  = perfdhcp.c
+
 
 
-perfdhcp_LDADD = $(top_builddir)/src/lib/exceptions/libexceptions.la

File diff suppressed because it is too large
+ 0 - 3565
tests/tools/perfdhcp/perfdhcp.cc