Browse Source

[master]Merge branch 'master' of ssh://git.bind10.isc.org/var/bind10/git/bind10

Jeremy C. Reed 13 years ago
parent
commit
f055c638d5

+ 21 - 0
AUTHORS

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

+ 7 - 0
ChangeLog

@@ -1,3 +1,10 @@
+445.	[bug]*		jinmei
+	The pre-install check for older SQLite3 DB now refers to the DB
+	file with the prefix of DESTDIR.  This ensures that 'make install'
+	with specific DESTDIR works regardless of the version of the DB
+	file installed in the default path.
+	(Trac #1982, git 380b3e8ec02ef45555c0113ee19329fe80539f71)
+
 444.	[bug]		jinmei
 	libdatasrc: fixed ZoneFinder for database-based data sources so
 	that it handles type DS query correctly, i.e., treating it as

+ 11 - 6
compatcheck/Makefile.am

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

+ 139 - 22
src/bin/ddns/ddns.py.in

@@ -23,13 +23,16 @@ import bind10_config
 from isc.dns import *
 import isc.ddns.session
 from isc.ddns.zone_config import ZoneConfig
-from isc.ddns.logger import ClientFormatter
+from isc.ddns.logger import ClientFormatter, ZoneFormatter
 from isc.config.ccsession import *
-from isc.cc import SessionError, SessionTimeout
+from isc.cc import SessionError, SessionTimeout, ProtocolError
 import isc.util.process
 import isc.util.cio.socketsession
+from isc.notify.notify_out import ZONE_NEW_DATA_READY_CMD
 import isc.server_common.tsig_keyring
+from isc.server_common.dns_tcp import DNSTCPContext
 from isc.datasrc import DataSourceClient
+from isc.server_common.auth_command import auth_loadzone_command
 import select
 import errno
 
@@ -79,6 +82,10 @@ AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + '/auth.spec'
 
 isc.util.process.rename()
 
+# Cooperating modules
+XFROUT_MODULE_NAME = 'Xfrout'
+AUTH_MODULE_NAME = 'Auth'
+
 class DDNSConfigError(Exception):
     '''An exception indicating an error in updating ddns configuration.
 
@@ -147,6 +154,10 @@ def get_datasrc_client(cc_session):
         return HARDCODED_DATASRC_CLASS, DummyDataSourceClient(ex)
 
 class DDNSServer:
+    # The number of TCP clients that can be handled by the server at the same
+    # time (this should be configurable parameter).
+    TCP_CLIENTS = 10
+
     def __init__(self, cc_session=None):
         '''
         Initialize the DDNS Server.
@@ -186,14 +197,16 @@ class DDNSServer:
         self.__request_msg = Message(Message.PARSE)
         self.__response_renderer = MessageRenderer()
 
-        # The following attribute(s) are essentially private and constant,
-        # but defined as "protected" so that test code can customize them.
-        # They should not be overridden for any other purposes.
+        # The following attribute(s) are essentially private, but defined as
+        # "protected" so that test code can customize/inspect them.
+        # They should not be overridden/referenced for any other purposes.
         #
         # DDNS Protocol handling class.
         self._UpdateSessionClass = isc.ddns.session.UpdateSession
+        # Outstanding TCP context: fileno=>(context_obj, dst)
+        self._tcp_ctxs = {}
 
-    class SessionError(Exception):
+    class InternalError(Exception):
         '''Exception for internal errors in an update session.
 
         This exception is expected to be caught within the server class,
@@ -301,8 +314,8 @@ class DDNSServer:
                                isc.server_common.tsig_keyring.get_keyring())
         tsig_error = tsig_ctx.verify(tsig_record, req_data)
         if tsig_error != TSIGError.NOERROR:
-            raise SessionError("Failed to verify request's TSIG: " +
-                               str(tsig_error))
+            raise self.InternalError("Failed to verify request's TSIG: " +
+                                     str(tsig_error))
         return tsig_ctx
 
     def handle_request(self, req_session):
@@ -338,14 +351,13 @@ class DDNSServer:
         # or dropped by the sender, so if such error is detected we treat it
         # as an internal error and don't bother to respond.
         try:
-            if sock.proto == socket.IPPROTO_TCP:
-                raise SessionError('TCP requests are not yet supported')
             self.__request_msg.clear(Message.PARSE)
             # specify PRESERVE_ORDER as we need to handle each RR separately.
             self.__request_msg.from_wire(req_data, Message.PRESERVE_ORDER)
             if self.__request_msg.get_opcode() != Opcode.UPDATE():
-                raise SessionError('Update request has unexpected opcode: ' +
-                                   str(self.__request_msg.get_opcode()))
+                raise self.InternalError('Update request has unexpected '
+                                         'opcode: ' +
+                                         str(self.__request_msg.get_opcode()))
             tsig_ctx = self.__check_request_tsig(self.__request_msg, req_data)
         except Exception as ex:
             logger.error(DDNS_REQUEST_PARSE_FAIL, ex)
@@ -371,29 +383,125 @@ class DDNSServer:
             msg.to_wire(self.__response_renderer, tsig_ctx)
         else:
             msg.to_wire(self.__response_renderer)
+
+        ret = self.__send_response(sock, self.__response_renderer.get_data(),
+                                   remote_addr)
+        if result == isc.ddns.session.UPDATE_SUCCESS:
+            self.__notify_auth(zname, zclass)
+            self.__notify_xfrout(zname, zclass)
+        return ret
+
+    def __send_response(self, sock, data, dest):
+        '''Send DDNS response to the client.
+
+        Right now, this is a straightforward subroutine of handle_request(),
+        but is intended to be extended evetually so that it can handle more
+        comlicated operations for TCP (which requires asynchronous write).
+        Further, when we support multiple requests over a single TCP
+        connection, this method may even be shared by multiple methods.
+
+        Parameters:
+        sock: (python socket) the socket to which the response should be sent.
+        data: (binary) the response data
+        dest: (python socket address) the destion address to which the response
+          should be sent.
+
+        Return: True if the send operation succeds; otherwise False.
+
+        '''
         try:
-            sock.sendto(self.__response_renderer.get_data(), remote_addr)
+            if sock.proto == socket.IPPROTO_UDP:
+                sock.sendto(data, dest)
+            else:
+                tcp_ctx = DNSTCPContext(sock)
+                send_result = tcp_ctx.send(data)
+                if send_result == DNSTCPContext.SENDING:
+                    self._tcp_ctxs[sock.fileno()] = (tcp_ctx, dest)
+                elif send_result == DNSTCPContext.CLOSED:
+                    raise socket.error("socket error in TCP send")
+                else:
+                    tcp_ctx.close()
         except socket.error as ex:
-            logger.error(DDNS_RESPONSE_SOCKET_ERROR,
-                         ClientFormatter(remote_addr), ex)
+            logger.warn(DDNS_RESPONSE_SOCKET_ERROR, ClientFormatter(dest), ex)
             return False
 
         return True
 
+    def __notify_auth(self, zname, zclass):
+        '''Notify auth of the update, if necessary.'''
+        msg = auth_loadzone_command(self._cc, zname, zclass)
+        if msg is not None:
+            self.__notify_update(AUTH_MODULE_NAME, msg, zname, zclass)
+
+    def __notify_xfrout(self, zname, zclass):
+        '''Notify xfrout of the update.'''
+        param = {'zone_name': zname.to_text(), 'zone_class': zclass.to_text()}
+        msg = create_command(ZONE_NEW_DATA_READY_CMD, param)
+        self.__notify_update(XFROUT_MODULE_NAME, msg, zname, zclass)
+
+    def __notify_update(self, modname, msg, zname, zclass):
+        '''Notify other module of the update.
+
+        Note that we use blocking communication here.  While the internal
+        communication bus is generally expected to be pretty responsive and
+        error free, notable delay can still occur, and in worse cases timeouts
+        or connection reset can happen.  In these cases, even if the trouble
+        is temporary, the update service will be suspended for a while.
+        For a longer term we'll need to switch to asynchronous communication,
+        but for now we rely on the blocking operation.
+
+        Note also that we directly refer to the "protected" member of
+        ccsession (_cc._session) rather than creating a separate channel.
+        It's probably not the best practice, but hopefully we can introduce
+        a cleaner way when we support asynchronous communication.
+        At the moment we prefer the brevity with the use of internal channel
+        of the cc session.
+
+        '''
+        try:
+            seq = self._cc._session.group_sendmsg(msg, modname)
+            answer, _ = self._cc._session.group_recvmsg(False, seq)
+            rcode, error_msg = parse_answer(answer)
+        except (SessionTimeout, SessionError, ProtocolError) as ex:
+            rcode = 1
+            error_msg = str(ex)
+        if rcode == 0:
+            logger.debug(TRACE_BASIC, DDNS_UPDATE_NOTIFY, modname,
+                         ZoneFormatter(zname, zclass))
+        else:
+            logger.error(DDNS_UPDATE_NOTIFY_FAIL, modname,
+                         ZoneFormatter(zname, zclass), error_msg)
+
     def handle_session(self, fileno):
-        """
-        Handle incoming session on the socket with given fileno.
+        """Handle incoming session on the socket with given fileno.
+
+        Return True if a response (whether positive or negative) has been
+        sent; otherwise False.  The return value isn't expected to be used
+        for other purposes than testing.
+
         """
         logger.debug(TRACE_BASIC, DDNS_SESSION, fileno)
-        (socket, receiver) = self._socksession_receivers[fileno]
+        (session_socket, receiver) = self._socksession_receivers[fileno]
         try:
-            self.handle_request(receiver.pop())
+            req_session = receiver.pop()
+            (sock, remote_addr) = (req_session[0], req_session[2])
+
+            # If this is a TCP client, check the quota, and immediately reject
+            # it if we cannot accept more.
+            if sock.proto == socket.IPPROTO_TCP and \
+                    len(self._tcp_ctxs) >= self.TCP_CLIENTS:
+                logger.warn(DDNS_REQUEST_TCP_QUOTA,
+                            ClientFormatter(remote_addr), len(self._tcp_ctxs))
+                sock.close()
+                return False
+            return self.handle_request(req_session)
         except isc.util.cio.socketsession.SocketSessionError as se:
             # No matter why this failed, the connection is in unknown, possibly
             # broken state. So, we close the socket and remove the receiver.
             del self._socksession_receivers[fileno]
-            socket.close()
+            session_socket.close()
             logger.warn(DDNS_DROP_CONN, fileno, se)
+            return False
 
     def run(self):
         '''
@@ -414,8 +522,8 @@ class DDNSServer:
             try:
                 (reads, writes, exceptions) = \
                     select.select([cc_fileno, listen_fileno] +
-                                  list(self._socksession_receivers.keys()), [],
-                                  [])
+                                  list(self._socksession_receivers.keys()),
+                                  list(self._tcp_ctxs.keys()), [])
             except select.error as se:
                 # In case it is just interrupted, we continue like nothing
                 # happened
@@ -430,6 +538,15 @@ class DDNSServer:
                     self.accept()
                 else:
                     self.handle_session(fileno)
+            for fileno in writes:
+                ctx = self._tcp_ctxs[fileno]
+                result = ctx[0].send_ready()
+                if result != DNSTCPContext.SENDING:
+                    if result == DNSTCPContext.CLOSED:
+                        logger.warn(DDNS_RESPONSE_TCP_SOCKET_ERROR,
+                                    ClientFormatter(ctx[1]))
+                    ctx[0].close()
+                    del self._tcp_ctxs[fileno]
         self.shutdown_cleanup()
         logger.info(DDNS_STOPPED)
 

+ 51 - 1
src/bin/ddns/ddns_messages.mes

@@ -81,11 +81,38 @@ this stage and should rather be considered an internal bug.  This
 event is therefore logged at the error level, and the request is
 simply dropped.  Additional information of the error is also logged.
 
+% DDNS_REQUEST_TCP_QUOTA reject TCP update client %1 (%2 running)
+b10-ddns received a new update request from a client over TCP, but
+the number of TCP clients being handled by the server already reached
+the configured quota, so the latest client was rejected by closing
+the connection.  The administrator may want to check the status of
+b10-ddns, and if this happens even if the server is not very busy,
+the quota may have to be increased.  Or, if it's more likely to be
+malicious or simply bogus clients that somehow keep the TCP connection
+open for a long period, maybe they should be rejected with an
+appropriate ACL configuration or some lower layer filtering.  The
+number of existing TCP clients are shown in the log, which should be
+identical to the current quota.
+
 % DDNS_RESPONSE_SOCKET_ERROR failed to send update response to %1: %2
-Network I/O error happens in sending an update request.  The
+Network I/O error happens in sending an update response.  The
 client's address that caused the error and error details are also
 logged.
 
+% DDNS_RESPONSE_TCP_SOCKET_ERROR failed to complete sending update response to %1 over TCP
+b10-ddns had tried to send an update response over TCP, and it hadn't
+been completed at that time, and a followup attempt to complete the
+send operation failed due to some network I/O error.  While a network
+error can happen any time, this event is quite unexpected for two
+reasons.  First, since the size of a response to an update request
+should be generally small, it's unlikely that the initial attempt
+didn't fail but wasn't completed.  Second, since the first attempt
+succeeded and the TCP connection had been established in the first
+place, it's more likely for the subsequent attempt to succeed.  In any
+case, there may not be able to do anything to fix it at the server
+side, but the administrator may want to check the general reachability
+with the client address.
+
 % DDNS_RUNNING ddns server is running and listening for updates
 The ddns process has successfully started and is now ready to receive commands
 and updates.
@@ -112,3 +139,26 @@ process will now shut down.
 The b10-ddns process encountered an uncaught exception and will now shut
 down. This is indicative of a programming error and should not happen under
 normal circumstances. The exception type and message are printed.
+
+% DDNS_UPDATE_NOTIFY notified %1 of updates to %2
+Debug message.  b10-ddns has made updates to a zone based on an update
+request and has successfully notified an external module of the updates.
+The notified module will use that information for updating its own
+state or any necessary protocol action such as zone reloading or sending
+notify messages to secondary servers.
+
+% DDNS_UPDATE_NOTIFY_FAIL failed to notify %1 of updates to %2: %3
+b10-ddns has made updates to a zone based on an update request and
+tried to notify an external module of the updates, but the
+notification fails.  Severity of this effect depends on the type of
+the module.  If it's b10-xfrout, this means DNS notify messages won't
+be sent to secondary servers of the zone.  It's suboptimal, but not
+necessarily critical as the secondary servers will try to check the
+zone's status periodically.  If it's b10-auth and the notification was
+needed to have it reload the corresponding zone, it's more serious
+because b10-auth won't be able to serve the new version of the zone
+unless some explicit recovery action is taken.  So the administrator
+needs to examine this message and takes an appropriate action.  In
+either case, this notification is generally expected to succeed; so
+the fact it fails itself means there's something wrong in the BIND 10
+system, and it would be advisable to check other log messages.

+ 397 - 12
src/bin/ddns/tests/ddns_test.py

@@ -19,7 +19,10 @@ from isc.ddns.session import *
 from isc.dns import *
 from isc.acl.acl import ACCEPT
 import isc.util.cio.socketsession
+from isc.cc.session import SessionTimeout, SessionError, ProtocolError
 from isc.datasrc import DataSourceClient
+from isc.config.ccsession import create_answer
+from isc.server_common.dns_tcp import DNSTCPContext
 import ddns
 import errno
 import os
@@ -57,17 +60,23 @@ class FakeSocket:
     """
     A fake socket. It only provides a file number, peer name and accept method.
     """
-    def __init__(self, fileno):
-        self.proto = socket.IPPROTO_UDP
+    def __init__(self, fileno, proto=socket.IPPROTO_UDP):
+        self.proto = proto
         self.__fileno = fileno
         self._sent_data = None
         self._sent_addr = None
+        self._close_called = 0  # number of calls to close()
+        self.__send_cc = 0      # waterline of the send buffer (can be reset)
         # customizable by tests; if set to True, sendto() will throw after
         # recording the parameters.
         self._raise_on_send = False
+        self._send_buflen = None # imaginary send buffer for partial send
     def fileno(self):
         return self.__fileno
     def getpeername(self):
+        if self.proto == socket.IPPROTO_UDP or \
+                self.proto == socket.IPPROTO_TCP:
+            return TEST_CLIENT4
         return "fake_unix_socket"
     def accept(self):
         return FakeSocket(self.__fileno + 1), '/dummy/path'
@@ -76,10 +85,39 @@ class FakeSocket:
         self._sent_addr = addr
         if self._raise_on_send:
             raise socket.error('test socket failure')
+    def send(self, data):
+        if self._raise_on_send:
+            raise socket.error(errno.EPIPE, 'faked connection disruption')
+        elif self._send_buflen is None:
+            available_space = len(data)
+        else:
+            available_space = self._send_buflen - self.__send_cc
+        if available_space == 0:
+            # if there's no space, (assuming it's nonblocking mode) raise
+            # EAGAIN.
+            raise socket.error(errno.EAGAIN,
+                               "Resource temporarily unavailable")
+        # determine the sendable part of the data, record it, update "buffer".
+        cc = min(available_space, len(data))
+        if self._sent_data is None:
+            self._sent_data = data[:cc]
+        else:
+            self._sent_data += data[:cc]
+        self.__send_cc += cc
+        return cc
+    def setblocking(self, on):
+        # We only need a faked NO-OP implementation.
+        pass
+    def close(self):
+        self._close_called += 1
     def clear(self):
         '''Clear internal instrumental data.'''
         self._sent_data = None
         self._sent_addr = None
+    def make_send_ready(self):
+        # pretend that the accrued data has been cleared, making room in
+        # the send buffer.
+        self.__send_cc = 0
 
 class FakeSessionReceiver:
     """
@@ -147,6 +185,10 @@ class FakeKeyringModule:
 
 class MyCCSession(isc.config.ConfigData):
     '''Fake session with minimal interface compliance.'''
+
+    # faked CC sequence used in group_send/recvmsg
+    FAKE_SEQUENCE = 53
+
     def __init__(self):
         module_spec = isc.config.module_spec_from_file(
             ddns.SPECFILE_LOCATION)
@@ -155,6 +197,16 @@ class MyCCSession(isc.config.ConfigData):
         self._stopped = False
         # Used as the return value of get_remote_config_value.  Customizable.
         self.auth_db_file = READ_ZONE_DB_FILE
+        # Used as the return value of get_remote_config_value.  Customizable.
+        self.auth_datasources = None
+        # faked cc channel, providing group_send/recvmsg itself.  The following
+        # attributes are for inspection/customization in tests.
+        self._session = self
+        self._sent_msg = []
+        self._recvmsg_called = 0
+        self._answer_code = 0   # code used in answer returned via recvmsg
+        self._sendmsg_exception = None # will be raised from sendmsg if !None
+        self._recvmsg_exception = None # will be raised from recvmsg if !None
 
     def start(self):
         '''Called by DDNSServer initialization, but not used in tests'''
@@ -176,6 +228,37 @@ class MyCCSession(isc.config.ConfigData):
     def get_remote_config_value(self, module_name, item):
         if module_name == "Auth" and item == "database_file":
             return self.auth_db_file, False
+        if module_name == "Auth" and item == "datasources":
+            if self.auth_datasources is None:
+                return [], True # default
+            else:
+                return self.auth_datasources, False
+
+    def group_sendmsg(self, msg, group):
+        # remember the passed parameter, and return dummy sequence
+        self._sent_msg.append((msg, group))
+        if self._sendmsg_exception is not None:
+            raise self._sendmsg_exception
+        return self.FAKE_SEQUENCE
+
+    def group_recvmsg(self, nonblock, seq):
+        self._recvmsg_called += 1
+        if seq != self.FAKE_SEQUENCE:
+            raise RuntimeError('unexpected CC sequence: ' + str(seq))
+        if self._recvmsg_exception is not None:
+            raise self._recvmsg_exception
+        if self._answer_code is 0:
+            return create_answer(0), None
+        else:
+            return create_answer(self._answer_code, "dummy error value"), None
+
+    def clear_msg(self):
+        '''Clear instrumental attributes related session messages.'''
+        self._sent_msg = []
+        self._recvmsg_called = 0
+        self._answer_code = 0
+        self._sendmsg_exception = None
+        self._recvmsg_exception = None
 
 class MyDDNSServer():
     '''Fake DDNS server used to test the main() function'''
@@ -219,6 +302,11 @@ class TestDDNSServer(unittest.TestCase):
         self.ddns_server._listen_socket = FakeSocket(2)
         ddns.select.select = self.__select
 
+        # common private attributes for TCP response tests
+        self.__tcp_sock = FakeSocket(10, socket.IPPROTO_TCP)
+        self.__tcp_ctx = DNSTCPContext(self.__tcp_sock)
+        self.__tcp_data = b'A' * 12 # dummy, just the same size as DNS header
+
     def tearDown(self):
         ddns.select.select = select.select
         ddns.isc.util.cio.socketsession.SocketSessionReceiver = \
@@ -547,6 +635,100 @@ class TestDDNSServer(unittest.TestCase):
         self.__select_expected = ([1, 2], [], [], None)
         self.assertRaises(select.error, self.ddns_server.run)
 
+    def __send_select_tcp(self, buflen, raise_after_select=False):
+        '''Common subroutine for some TCP related tests below.'''
+        fileno = self.__tcp_sock.fileno()
+        self.ddns_server._tcp_ctxs = {fileno: (self.__tcp_ctx, TEST_CLIENT6)}
+
+        # make an initial, incomplete send via the test context
+        self.__tcp_sock._send_buflen = buflen
+        self.assertEqual(DNSTCPContext.SENDING,
+                         self.__tcp_ctx.send(self.__tcp_data))
+        self.assertEqual(buflen, len(self.__tcp_sock._sent_data))
+        # clear the socket "send buffer"
+        self.__tcp_sock.make_send_ready()
+        # if requested, set up exception
+        self.__tcp_sock._raise_on_send = raise_after_select
+
+        # Run select
+        self.__select_expected = ([1, 2], [fileno], [], None)
+        self.__select_answer = ([], [fileno], [])
+        self.ddns_server.run()
+
+    def test_select_send_continued(self):
+        '''Test continuation of sending a TCP response.'''
+        # Common setup, with the bufsize that would make it complete after a
+        # single select call.
+        self.__send_select_tcp(7)
+
+        # Now the send should be completed.  socket should be closed,
+        # and the context should be removed from the server.
+        self.assertEqual(14, len(self.__tcp_sock._sent_data))
+        self.assertEqual(1, self.__tcp_sock._close_called)
+        self.assertEqual(0, len(self.ddns_server._tcp_ctxs))
+
+    def test_select_send_continued_twice(self):
+        '''Test continuation of sending a TCP response, still continuing.'''
+        # This is similar to the send_continued test, but the continued
+        # operation still won't complete the send.
+        self.__send_select_tcp(5)
+
+        # Only 10 bytes should have been transmitted, socket is still open,
+        # and the context is still in the server (that would require select
+        # watch it again).
+        self.assertEqual(10, len(self.__tcp_sock._sent_data))
+        self.assertEqual(0, self.__tcp_sock._close_called)
+        fileno = self.__tcp_sock.fileno()
+        self.assertEqual(self.__tcp_ctx,
+                         self.ddns_server._tcp_ctxs[fileno][0])
+
+    def test_select_send_continued_failed(self):
+        '''Test continuation of sending a TCP response, which fails.'''
+        # Let the socket raise an exception in the second call to send().
+        self.__send_select_tcp(5, raise_after_select=True)
+
+        # Only the data before select() have been transmitted, socket is
+        # closed due to the failure, and the context is removed from the
+        # server.
+        self.assertEqual(5, len(self.__tcp_sock._sent_data))
+        self.assertEqual(1, self.__tcp_sock._close_called)
+        self.assertEqual(0, len(self.ddns_server._tcp_ctxs))
+
+    def test_select_multi_tcp(self):
+        '''Test continuation of sending a TCP response, multiple sockets.'''
+        # Check if the implementation still works with multiple outstanding
+        # TCP contexts.  We use three (arbitray choice), of which two will be
+        # writable after select and complete the send.
+        tcp_socks = []
+        for i in range(0, 3):
+            # Use faked FD of 100, 101, 102 (again, arbitrary choice)
+            s = FakeSocket(100 + i, proto=socket.IPPROTO_TCP)
+            ctx = DNSTCPContext(s)
+            self.ddns_server._tcp_ctxs[s.fileno()] = (ctx, TEST_CLIENT6)
+            s._send_buflen = 7  # make sure it requires two send's
+            self.assertEqual(DNSTCPContext.SENDING, ctx.send(self.__tcp_data))
+            s.make_send_ready()
+
+            tcp_socks.append(s)
+
+        self.__select_expected = ([1, 2], [100, 101, 102], [], None)
+        self.__select_answer = ([], [100, 102], [])
+        self.ddns_server.run()
+
+        for i in [0, 2]:
+            self.assertEqual(14, len(tcp_socks[i]._sent_data))
+            self.assertEqual(1, tcp_socks[i]._close_called)
+        self.assertEqual(1, len(self.ddns_server._tcp_ctxs))
+
+    def test_select_bad_writefd(self):
+        # There's no outstanding TCP context, but select somehow returns
+        # writable FD.  It should result in an uncaught exception, killing
+        # the server.  This is okay, because it shouldn't happen and should be
+        # an internal bug.
+        self.__select_expected = ([1, 2], [], [], None)
+        self.__select_answer = ([], [10], [])
+        self.assertRaises(KeyError, self.ddns_server.run)
+
 def create_msg(opcode=Opcode.UPDATE(), zones=[TEST_ZONE_RECORD], prereq=[],
                tsigctx=None):
     msg = Message(Message.RENDER)
@@ -571,13 +753,13 @@ def create_msg(opcode=Opcode.UPDATE(), zones=[TEST_ZONE_RECORD], prereq=[],
     return renderer.get_data()
 
 
-class TestDDNSession(unittest.TestCase):
+class TestDDNSSession(unittest.TestCase):
     def setUp(self):
-        cc_session = MyCCSession()
-        self.assertFalse(cc_session._started)
+        self.__cc_session = MyCCSession()
+        self.assertFalse(self.__cc_session._started)
         self.orig_tsig_keyring = isc.server_common.tsig_keyring
         isc.server_common.tsig_keyring = FakeKeyringModule()
-        self.server = ddns.DDNSServer(cc_session)
+        self.server = ddns.DDNSServer(self.__cc_session)
         self.server._UpdateSessionClass = self.__fake_session_creator
         self.__faked_result = UPDATE_SUCCESS # will be returned by fake session
         self.__sock = FakeSocket(-1)
@@ -593,7 +775,7 @@ class TestDDNSession(unittest.TestCase):
                                  self.__faked_result)
 
     def check_update_response(self, resp_wire, expected_rcode=Rcode.NOERROR(),
-                              tsig_ctx=None):
+                              tsig_ctx=None, tcp=False):
         '''Check if given wire data are valid form of update response.
 
         In this implementation, zone/prerequisite/update sections should be
@@ -603,7 +785,15 @@ class TestDDNSession(unittest.TestCase):
         be TSIG signed and the signature should be verifiable with the context
         that has signed the corresponding request.
 
+        if tcp is True, the wire data are expected to be prepended with
+        a 2-byte length field.
+
         '''
+        if tcp:
+            data_len = resp_wire[0] * 256 + resp_wire[1]
+            resp_wire = resp_wire[2:]
+            self.assertEqual(len(resp_wire), data_len)
+
         msg = Message(Message.PARSE)
         msg.from_wire(resp_wire)
         if tsig_ctx is not None:
@@ -676,19 +866,98 @@ class TestDDNSession(unittest.TestCase):
         self.__sock._raise_on_send = True
         # handle_request indicates the failure
         self.assertFalse(self.server.handle_request((self.__sock, TEST_SERVER6,
-                                                     TEST_SERVER4,
+                                                     TEST_CLIENT6,
                                                      create_msg())))
         # this check ensures sendto() was really attempted.
         self.check_update_response(self.__sock._sent_data, Rcode.NOERROR())
 
     def test_tcp_request(self):
-        # Right now TCP request is not supported.
+        # A simple case using TCP: all resopnse data are sent out at once.
         s = self.__sock
         s.proto = socket.IPPROTO_TCP
+        self.assertTrue(self.server.handle_request((s, TEST_SERVER6,
+                                                    TEST_CLIENT6,
+                                                    create_msg())))
+        self.check_update_response(s._sent_data, Rcode.NOERROR(), tcp=True)
+        # In the current implementation, the socket should be closed
+        # immedidately after a successful send.
+        self.assertEqual(1, s._close_called)
+        # TCP context shouldn't be held in the server.
+        self.assertEqual(0, len(self.server._tcp_ctxs))
+
+    def test_tcp_request_incomplete(self):
+        # set the size of the socket "send buffer" to a small value, which
+        # should cause partial send.
+        s = self.__sock
+        s.proto = socket.IPPROTO_TCP
+        s._send_buflen = 7
+        # before this request there should be no outstanding TCP context.
+        self.assertEqual(0, len(self.server._tcp_ctxs))
+        self.assertTrue(self.server.handle_request((s, TEST_SERVER6,
+                                                    TEST_CLIENT6,
+                                                    create_msg())))
+        # Only the part of data that fit the send buffer should be transmitted.
+        self.assertEqual(s._send_buflen, len(s._sent_data))
+        # the socket is not yet closed.
+        self.assertEqual(0, s._close_called)
+        # and a new context is stored in the server.
+        self.assertEqual(1, len(self.server._tcp_ctxs))
+
+        # clear the "send buffer" of the fake socket, and continue the send
+        # by hand.  The next attempt should complete the send, and the combined
+        # data should be the expected response.
+        s.make_send_ready()
+        self.assertEqual(DNSTCPContext.SEND_DONE,
+                         self.server._tcp_ctxs[s.fileno()][0].send_ready())
+        self.check_update_response(s._sent_data, Rcode.NOERROR(), tcp=True)
+
+    def test_tcp_request_error(self):
+        # initial send() on the TCP socket will fail.  The request handling
+        # will be considered failure.
+        s = self.__sock
+        s.proto = socket.IPPROTO_TCP
+        s._raise_on_send = True
         self.assertFalse(self.server.handle_request((s, TEST_SERVER6,
-                                                     TEST_SERVER4,
+                                                     TEST_CLIENT6,
                                                      create_msg())))
-        self.assertEqual((None, None), (s._sent_data, s._sent_addr))
+        # the socket should have been closed.
+        self.assertEqual(1, s._close_called)
+
+    def test_tcp_request_quota(self):
+        '''Test'''
+        # Originally the TCP context map should be empty.
+        self.assertEqual(0, len(self.server._tcp_ctxs))
+
+        class FakeReceiver:
+            '''Faked SessionReceiver, just returning given param by pop()'''
+            def __init__(self, param):
+                self.__param = param
+            def pop(self):
+                return self.__param
+
+        def check_tcp_ok(fd, expect_grant):
+            '''Supplemental checker to see if TCP request is handled.'''
+            s = FakeSocket(fd, proto=socket.IPPROTO_TCP)
+            s._send_buflen = 7
+            self.server._socksession_receivers[s.fileno()] = \
+                (None, FakeReceiver((s, TEST_SERVER6, TEST_CLIENT6,
+                                     create_msg())))
+            self.assertEqual(expect_grant,
+                             self.server.handle_session(s.fileno()))
+            self.assertEqual(0 if expect_grant else 1, s._close_called)
+
+        # By default up to 10 TCP clients can coexist (use hardcode
+        # intentionally so we can test the default value itself)
+        for i in range(0, 10):
+            check_tcp_ok(i, True)
+        self.assertEqual(10, len(self.server._tcp_ctxs))
+
+        # Beyond that, it should be rejected (by reset)
+        check_tcp_ok(11, False)
+
+        # If we remove one context from the server, new client can go in again.
+        self.server._tcp_ctxs.pop(5)
+        check_tcp_ok(12, True)
 
     def test_request_message(self):
         '''Test if the request message stores RRs separately.'''
@@ -705,8 +974,124 @@ class TestDDNSession(unittest.TestCase):
         num_rrsets = len(self.__req_message.get_section(SECTION_PREREQUISITE))
         self.assertEqual(2, num_rrsets)
 
+    def check_session_msg(self, result, expect_recv=1, notify_auth=False):
+        '''Check post update communication with other modules.'''
+        # iff the update succeeds, b10-ddns should tell interested other
+        # modules the information about the update zone.  Possible modules
+        # are xfrout and auth: for xfrout, the message format should be:
+        # {'command': ['notify', {'zone_name': <updated_zone_name>,
+        #                         'zone_class', <updated_zone_class>}]}
+        # for auth, it should be:
+        # {'command': ['loadzone', {'origin': <updated_zone_name>,
+        #                           'class', <updated_zone_class>,
+        #                           'datasrc', <datasrc type, should be
+        #                                       "memory" in practice>}]}
+        # and expect an answer by calling group_recvmsg().
+        #
+        # expect_recv indicates the expected number of calls to
+        # group_recvmsg(), which is normally 1, but can be 0 if send fails;
+        # if the message is to be sent
+        if result == UPDATE_SUCCESS:
+            expected_sentmsg = 2 if notify_auth else 1
+            self.assertEqual(expected_sentmsg,
+                             len(self.__cc_session._sent_msg))
+            self.assertEqual(expect_recv, self.__cc_session._recvmsg_called)
+            msg_cnt = 0
+            if notify_auth:
+                sent_msg, sent_group = self.__cc_session._sent_msg[msg_cnt]
+                sent_cmd = sent_msg['command']
+                self.assertEqual('Auth', sent_group)
+                self.assertEqual('loadzone', sent_cmd[0])
+                self.assertEqual(3, len(sent_cmd[1]))
+                self.assertEqual(TEST_ZONE_NAME.to_text(),
+                                 sent_cmd[1]['origin'])
+                self.assertEqual(TEST_RRCLASS.to_text(),
+                                 sent_cmd[1]['class'])
+                self.assertEqual('memory', sent_cmd[1]['datasrc'])
+                msg_cnt += 1
+            sent_msg, sent_group = self.__cc_session._sent_msg[msg_cnt]
+            sent_cmd = sent_msg['command']
+            self.assertEqual('Xfrout', sent_group)
+            self.assertEqual('notify', sent_cmd[0])
+            self.assertEqual(2, len(sent_cmd[1]))
+            self.assertEqual(TEST_ZONE_NAME.to_text(), sent_cmd[1]['zone_name'])
+            self.assertEqual(TEST_RRCLASS.to_text(), sent_cmd[1]['zone_class'])
+        else:
+            # for other result cases neither send nor recvmsg should be called.
+            self.assertEqual([], self.__cc_session._sent_msg)
+            self.assertEqual(0, self.__cc_session._recvmsg_called)
+
+    def test_session_msg(self):
+        '''Test post update communication with other modules.'''
+        # Normal cases, confirming communication takes place iff update
+        # succeeds
+        for r in [UPDATE_SUCCESS, UPDATE_ERROR, UPDATE_DROP]:
+            self.__cc_session.clear_msg()
+            self.check_session(result=r)
+            self.check_session_msg(r)
+
+        # Return an error from the remote module, which should be just ignored.
+        self.__cc_session.clear_msg()
+        self.__cc_session._answer_code = 1
+        self.check_session()
+        self.check_session_msg(UPDATE_SUCCESS)
+
+        # raise some exceptions from the faked session.  Expected ones are
+        # simply (logged and) ignored
+        self.__cc_session.clear_msg()
+        self.__cc_session._recvmsg_exception = SessionTimeout('dummy timeout')
+        self.check_session()
+        self.check_session_msg(UPDATE_SUCCESS)
+
+        self.__cc_session.clear_msg()
+        self.__cc_session._recvmsg_exception = SessionError('dummy error')
+        self.check_session()
+        self.check_session_msg(UPDATE_SUCCESS)
+
+        self.__cc_session.clear_msg()
+        self.__cc_session._recvmsg_exception = ProtocolError('dummy perror')
+        self.check_session()
+        self.check_session_msg(UPDATE_SUCCESS)
+
+        # Similar to the previous cases, but sendmsg() raises, so there should
+        # be no call to recvmsg().
+        self.__cc_session.clear_msg()
+        self.__cc_session._sendmsg_exception = SessionError('send error')
+        self.check_session()
+        self.check_session_msg(UPDATE_SUCCESS, expect_recv=0)
+
+        # Unexpected exception will be propagated (and will terminate the
+        # server)
+        self.__cc_session.clear_msg()
+        self.__cc_session._sendmsg_exception = RuntimeError('unexpected')
+        self.assertRaises(RuntimeError, self.check_session)
+
+    def test_session_msg_for_auth(self):
+        '''Test post update communication with other modules including Auth.'''
+        # Let the CC session return in-memory config with sqlite3 backend.
+        # (The default case was covered by other tests.)
+        self.__cc_session.auth_datasources = \
+            [{'type': 'memory', 'class': 'IN', 'zones': [
+                    {'origin': TEST_ZONE_NAME_STR, 'filetype': 'sqlite3'}]}]
+        self.check_session()
+        self.check_session_msg(UPDATE_SUCCESS, expect_recv=2, notify_auth=True)
+
+        # Let sendmsg() raise an exception.  The first exception shouldn't
+        # stop sending the second message.  There's just no recv calls.
+        self.__cc_session.clear_msg()
+        self.__cc_session._sendmsg_exception = SessionError('send error')
+        self.check_session()
+        self.check_session_msg(UPDATE_SUCCESS, expect_recv=0, notify_auth=True)
+
+        # Likewise, in the case recvmsg() raises (and there should be recv
+        # calls in this case)
+        self.__cc_session.clear_msg()
+        self.__cc_session._recvmsg_exception = SessionError('recv error')
+        self.check_session()
+        self.check_session_msg(UPDATE_SUCCESS, expect_recv=2, notify_auth=True)
+
     def test_session_with_config(self):
-        '''Check a session with more relistic config setups
+        '''Check a session with more realistic config setups.
 
         We don't have to explore various cases in detail in this test.
         We're just checking if the expected configured objects are passed

+ 8 - 44
src/bin/xfrin/xfrin.py.in

@@ -33,6 +33,7 @@ import isc.util.process
 from isc.datasrc import DataSourceClient, ZoneFinder
 import isc.net.parse
 from isc.xfrin.diff import Diff
+from isc.server_common.auth_command import auth_loadzone_command
 from isc.log_messages.xfrin_messages import *
 
 isc.log.init("b10-xfrin")
@@ -1248,50 +1249,13 @@ class ZoneInfo:
                 (str(self.master_addr), self.master_port))
 
 def _do_auth_loadzone(server, zone_name, zone_class):
-    # On a successful zone transfer, if the zone is served by
-    # b10-auth in the in-memory data source using sqlite3 as a
-    # backend, send the "loadzone" command for the zone to auth.
-    datasources, is_default =\
-        server._module_cc.get_remote_config_value(AUTH_MODULE_NAME, "datasources")
-    if is_default:
-        return
-    for d in datasources:
-        if "type" not in d:
-            continue
-        try:
-            if "class" in d:
-                dclass = RRClass(d["class"])
-            else:
-                dclass = RRClass("IN")
-        except InvalidRRClass as err:
-            logger.info(XFRIN_AUTH_CONFIG_RRCLASS_ERROR, str(err))
-            continue
-
-        if d["type"].lower() == "memory" and dclass == zone_class:
-            for zone in d["zones"]:
-                if "filetype" not in zone:
-                    continue
-                if "origin" not in zone:
-                    continue
-                if "filetype" not in zone:
-                    continue
-                try:
-                    name = Name(zone["origin"])
-                except (EmptyLabel, TooLongLabel, BadLabelType, BadEscape, TooLongName, IncompleteName):
-                    logger.info(XFRIN_AUTH_CONFIG_NAME_PARSER_ERROR, str(err))
-                    continue
-
-                if zone["filetype"].lower() == "sqlite3" and name == zone_name:
-                    param = {"origin": zone_name.to_text(),
-                             "class": zone_class.to_text(),
-                             "datasrc": d["type"]}
-
-                    logger.debug(DBG_XFRIN_TRACE, XFRIN_AUTH_LOADZONE,
-                                 param["origin"], param["class"], param["datasrc"])
-
-                    msg = create_command("loadzone", param)
-                    seq = server._send_cc_session.group_sendmsg(msg, AUTH_MODULE_NAME)
-                    answer, env = server._send_cc_session.group_recvmsg(False, seq)
+    msg = auth_loadzone_command(server._module_cc, zone_name, zone_class)
+    if msg is not None:
+        param = msg['command'][1]
+        logger.debug(DBG_XFRIN_TRACE, XFRIN_AUTH_LOADZONE, param["origin"],
+                     param["class"], param["datasrc"])
+        seq = server._send_cc_session.group_sendmsg(msg, AUTH_MODULE_NAME)
+        answer, env = server._send_cc_session.group_recvmsg(False, seq)
 
 class Xfrin:
     def __init__(self):

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

@@ -15,12 +15,6 @@
 # No namespace declaration - these constants go in the global namespace
 # of the xfrin messages python module.
 
-% XFRIN_AUTH_CONFIG_NAME_PARSER_ERROR Invalid name when parsing Auth configuration: %1
-There was an invalid name when parsing Auth configuration.
-
-% XFRIN_AUTH_CONFIG_RRCLASS_ERROR Invalid RRClass when parsing Auth configuration: %1
-There was an invalid RR class when parsing Auth configuration.
-
 % XFRIN_AUTH_LOADZONE sending Auth loadzone for origin=%1, class=%2, datasrc=%3
 There was a successful zone transfer, and the zone is served by b10-auth
 in the in-memory data source using sqlite3 as a backend. We send the

+ 16 - 2
src/lib/datasrc/Makefile.am

@@ -12,8 +12,13 @@ pkglibdir = $(libexecdir)/@PACKAGE@/backends
 datasrc_config.h: datasrc_config.h.pre
 	$(SED) -e "s|@@PKGLIBDIR@@|$(pkglibdir)|" datasrc_config.h.pre >$@
 
+static.zone: static.zone.pre
+	$(SED) -e "s|@@VERSION_STRING@@|$(PACKAGE_STRING)|" $(srcdir)/static.zone.pre >$@
+	$(SED) -e 's/\(.*\)/AUTHORS.BIND.	0	CH	TXT	"\1"/' $(top_srcdir)/AUTHORS >>$@
+
 CLEANFILES = *.gcno *.gcda datasrc_messages.h datasrc_messages.cc
 CLEANFILES += datasrc_config.h
+CLEANFILES += static.zone
 
 lib_LTLIBRARIES = libdatasrc.la
 libdatasrc_la_SOURCES = data_source.h data_source.cc
@@ -33,7 +38,7 @@ libdatasrc_la_SOURCES += factory.h factory.cc
 nodist_libdatasrc_la_SOURCES = datasrc_messages.h datasrc_messages.cc
 libdatasrc_la_LDFLAGS = -no-undefined -version-info 1:0:1
 
-pkglib_LTLIBRARIES =  sqlite3_ds.la memory_ds.la
+pkglib_LTLIBRARIES =  sqlite3_ds.la memory_ds.la static_ds.la
 
 sqlite3_ds_la_SOURCES = sqlite3_accessor.h sqlite3_accessor.cc
 sqlite3_ds_la_SOURCES += sqlite3_accessor_link.cc
@@ -49,6 +54,12 @@ memory_ds_la_LDFLAGS = -module -avoid-version
 memory_ds_la_LIBADD = $(top_builddir)/src/lib/exceptions/libexceptions.la
 memory_ds_la_LIBADD += libdatasrc.la
 
+static_ds_la_SOURCES = memory_datasrc.h memory_datasrc.cc
+static_ds_la_SOURCES += static_datasrc_link.cc
+static_ds_la_LDFLAGS = -module -avoid-version
+static_ds_la_LIBADD = $(top_builddir)/src/lib/exceptions/libexceptions.la
+static_ds_la_LIBADD += libdatasrc.la
+
 libdatasrc_la_LIBADD = $(top_builddir)/src/lib/exceptions/libexceptions.la
 libdatasrc_la_LIBADD += $(top_builddir)/src/lib/dns/libdns++.la
 libdatasrc_la_LIBADD += $(top_builddir)/src/lib/log/liblog.la
@@ -59,4 +70,7 @@ BUILT_SOURCES = datasrc_config.h datasrc_messages.h datasrc_messages.cc
 datasrc_messages.h datasrc_messages.cc: Makefile datasrc_messages.mes
 	$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/datasrc/datasrc_messages.mes
 
-EXTRA_DIST = datasrc_messages.mes
+EXTRA_DIST = datasrc_messages.mes static.zone.pre
+
+zonedir = $(pkgdatadir)
+zone_DATA = static.zone

+ 12 - 0
src/lib/datasrc/static.zone.pre

@@ -0,0 +1,12 @@
+;; This is the content of the BIND./CH zone. It contains the version and
+;; authors (called VERSION.BIND. and AUTHORS.BIND.). You can add more or
+;; modify the zone. Then you can reload the zone by issuing the command
+;;
+;;   loadzone CH BIND
+;;
+;; in the bindctl.
+
+BIND.           0   CH  SOA bind. authors.bind. 0 28800 7200 604800 86400
+
+VERSION.BIND.   0   CH  TXT "@@VERSION_STRING@@"
+;; HOSTNAME.BIND    0   CH  TXT "localhost"

+ 62 - 0
src/lib/datasrc/static_datasrc_link.cc

@@ -0,0 +1,62 @@
+// Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include "client.h"
+#include "memory_datasrc.h"
+
+#include <cc/data.h>
+#include <dns/rrclass.h>
+
+#include <memory>
+#include <exception>
+
+using namespace isc::data;
+using namespace isc::dns;
+using namespace boost;
+using namespace std;
+
+namespace isc {
+namespace datasrc {
+
+DataSourceClient*
+createInstance(ConstElementPtr config, string& error) {
+    try {
+        // Create the data source
+        auto_ptr<InMemoryClient> client(new InMemoryClient());
+        // Hardcode the origin and class
+        shared_ptr<InMemoryZoneFinder>
+            finder(new InMemoryZoneFinder(RRClass::CH(), Name("BIND")));
+        // Fill it with data
+        const string path(config->stringValue());
+        finder->load(path);
+        // And put the zone inside
+        client->addZone(finder);
+        return (client.release());
+    }
+    catch (const std::exception& e) {
+        error = e.what();
+    }
+    catch (...) {
+        error = "Unknown exception";
+    }
+    return (NULL);
+}
+
+void
+destroyInstance(DataSourceClient* instance) {
+    delete instance;
+}
+
+}
+}

+ 1 - 0
src/lib/datasrc/tests/Makefile.am

@@ -113,3 +113,4 @@ EXTRA_DIST += testdata/test.sqlite3
 EXTRA_DIST += testdata/new_minor_schema.sqlite3
 EXTRA_DIST += testdata/newschema.sqlite3
 EXTRA_DIST += testdata/oldschema.sqlite3
+EXTRA_DIST += testdata/static.zone

+ 56 - 0
src/lib/datasrc/tests/factory_unittest.cc

@@ -28,6 +28,8 @@ using namespace isc::datasrc;
 using namespace isc::data;
 
 std::string SQLITE_DBFILE_EXAMPLE_ORG = TEST_DATA_DIR "/example.org.sqlite3";
+const std::string STATIC_DS_FILE = TEST_DATA_DIR "/static.zone";
+const std::string ROOT_ZONE_FILE = TEST_DATA_DIR "/root.zone";
 
 namespace {
 
@@ -235,5 +237,59 @@ TEST(FactoryTest, badType) {
                                            DataSourceError);
 }
 
+// Check the static data source can be loaded.
+TEST(FactoryTest, staticDS) {
+    // The only configuration is the file to load.
+    const ConstElementPtr config(new StringElement(STATIC_DS_FILE));
+    // Get the data source
+    DataSourceClientContainer dsc("static", config);
+    // And try getting something out to see if it really works.
+    DataSourceClient::FindResult
+        result(dsc.getInstance().findZone(isc::dns::Name("BIND")));
+    ASSERT_EQ(result::SUCCESS, result.code);
+    EXPECT_EQ(isc::dns::Name("BIND"), result.zone_finder->getOrigin());
+    EXPECT_EQ(isc::dns::RRClass::CH(), result.zone_finder->getClass());
+    const isc::dns::ConstRRsetPtr
+        version(result.zone_finder->find(isc::dns::Name("VERSION.BIND"),
+                                         isc::dns::RRType::TXT())->rrset);
+    ASSERT_NE(isc::dns::ConstRRsetPtr(), version);
+    EXPECT_EQ(isc::dns::Name("VERSION.BIND"), version->getName());
+    EXPECT_EQ(isc::dns::RRClass::CH(), version->getClass());
+    EXPECT_EQ(isc::dns::RRType::TXT(), version->getType());
+}
+
+// Check that file not containing BIND./CH is rejected
+//
+// FIXME: This test is disabled because the InMemoryZoneFinder::load does
+// not check if the data loaded correspond with the origin. The static
+// factory is not the place to fix that.
+TEST(FactoryTest, DISABLED_staticDSBadFile) {
+    // The only configuration is the file to load.
+    const ConstElementPtr config(new StringElement(STATIC_DS_FILE));
+    // See it does not want the file
+    EXPECT_THROW(DataSourceClientContainer("static", config), DataSourceError);
+}
+
+// Check that some bad configs are rejected
+TEST(FactoryTest, staticDSBadConfig) {
+    const char* configs[] = {
+        // The file does not exist
+        "\"/does/not/exist\"",
+        // Bad types
+        "null",
+        "42",
+        "{}",
+        "[]",
+        "true",
+        NULL
+    };
+    for (const char** config(configs); *config; ++config) {
+        SCOPED_TRACE(*config);
+        EXPECT_THROW(DataSourceClientContainer("static",
+                                               Element::fromJSON(*config)),
+                     DataSourceError);
+    }
+}
+
 } // end anonymous namespace
 

+ 2 - 0
src/lib/datasrc/tests/testdata/static.zone

@@ -0,0 +1,2 @@
+BIND.           3600    CH  SOA BIND. BIND. 1 3600 300 36000 3600
+VERSION.BIND.   3600    CH  TXT "10"

+ 2 - 2
src/lib/dns/python/name_python.cc

@@ -115,7 +115,7 @@ PyObject* Name_reverse(s_Name* self);
 PyObject* Name_concatenate(s_Name* self, PyObject* args);
 PyObject* Name_downcase(s_Name* self);
 PyObject* Name_isWildCard(s_Name* self);
-long Name_hash(PyObject* py_self);
+Py_hash_t Name_hash(PyObject* py_self);
 
 PyMethodDef Name_methods[] = {
     { "at", reinterpret_cast<PyCFunction>(Name_at), METH_VARARGS,
@@ -520,7 +520,7 @@ Name_isWildCard(s_Name* self) {
     }
 }
 
-long
+Py_hash_t
 Name_hash(PyObject* pyself) {
     s_Name* const self = static_cast<s_Name*>(pyself);
     return (LabelSequence(*self->cppobj).getHash(false));

+ 5 - 0
src/lib/dns/python/pydnspp_common.h

@@ -43,6 +43,11 @@ extern PyObject* po_DNSMessageBADVERS;
 int readDataFromSequence(uint8_t *data, size_t len, PyObject* sequence);
 
 int addClassVariable(PyTypeObject& c, const char* name, PyObject* obj);
+
+// Short term workaround for unifying the return type of tp_hash
+#if PY_MINOR_VERSION < 2
+typedef long Py_hash_t;
+#endif
 } // namespace python
 } // namespace dns
 } // namespace isc

+ 2 - 2
src/lib/dns/python/rrclass_python.cc

@@ -52,7 +52,7 @@ PyObject* RRClass_str(PyObject* self);
 PyObject* RRClass_toWire(s_RRClass* self, PyObject* args);
 PyObject* RRClass_getCode(s_RRClass* self);
 PyObject* RRClass_richcmp(s_RRClass* self, s_RRClass* other, int op);
-long RRClass_hash(PyObject* pyself);
+Py_hash_t RRClass_hash(PyObject* pyself);
 
 // Static function for direct class creation
 PyObject* RRClass_IN(s_RRClass *self);
@@ -265,7 +265,7 @@ PyObject* RRClass_ANY(s_RRClass*) {
     return (RRClass_createStatic(RRClass::ANY()));
 }
 
-long
+Py_hash_t
 RRClass_hash(PyObject* pyself) {
     s_RRClass* const self = static_cast<s_RRClass*>(pyself);
     return (self->cppobj->getCode());

+ 1 - 1
src/lib/python/isc/ddns/libddns_messages.mes

@@ -205,7 +205,7 @@ should give more information on what prerequisite type failed.
 If the result code is FORMERR, the prerequisite section was not well-formed.
 An error response with the given result code is sent back to the client.
 
-% LIBDDNS_UPDATE_UNCAUGHT_EXCEPTION update client %1 for zone %2: uncaught exception while processing update section: %1
+% LIBDDNS_UPDATE_UNCAUGHT_EXCEPTION update client %1 for zone %2: uncaught exception while processing update section: %3
 An uncaught exception was encountered while processing the Update
 section of a DDNS message. The specific exception is shown in the log message.
 To make sure DDNS service is not interrupted, this problem is caught instead

+ 134 - 72
src/lib/python/isc/ddns/session.py

@@ -125,6 +125,58 @@ def collect_rrsets(collection, rrset):
     if not found:
         collection.append(rrset)
 
+class DDNS_SOA:
+    '''Class to handle the SOA in the DNS update '''
+
+    def __get_serial_internal(self, origin_soa):
+        '''Get serial number from soa'''
+        return Serial(int(origin_soa.get_rdata()[0].to_text().split()[2]))
+
+    def __write_soa_internal(self, origin_soa, soa_num):
+        '''Write back serial number to soa'''
+        new_soa = RRset(origin_soa.get_name(), origin_soa.get_class(),
+                        RRType.SOA(), origin_soa.get_ttl())
+        soa_rdata_parts = origin_soa.get_rdata()[0].to_text().split()
+        soa_rdata_parts[2] = str(soa_num.get_value())
+        new_soa.add_rdata(Rdata(origin_soa.get_type(), origin_soa.get_class(),
+                                " ".join(soa_rdata_parts)))
+        return new_soa
+
+    def soa_update_check(self, origin_soa, new_soa):
+        '''Check whether the new soa is valid. If the serial number is bigger
+        than the old one, it is valid, then return True, otherwise, return
+        False. Make sure the origin_soa and new_soa parameters are not none
+        before invoke soa_update_check.
+        Parameters:
+            origin_soa, old SOA resource record.
+            new_soa, new SOA resource record.
+        Output:
+            if the serial number of new soa is bigger than the old one, return
+            True, otherwise return False.
+        '''
+        old_serial = self.__get_serial_internal(origin_soa)
+        new_serial = self.__get_serial_internal(new_soa)
+        if(new_serial > old_serial):
+            return True
+        else:
+            return False
+
+    def update_soa(self, origin_soa, inc_number = 1):
+        ''' Update the soa number incrementally as RFC 2136. Please make sure
+        that the origin_soa exists and not none before invoke this function.
+        Parameters:
+            origin_soa, the soa resource record which will be updated.
+            inc_number, the number which will be added into the serial number of
+            origin_soa, the default value is one.
+        Output:
+            The new origin soa whoes serial number has been updated.
+        '''
+        soa_num = self.__get_serial_internal(origin_soa)
+        soa_num = soa_num + inc_number
+        if soa_num.get_value() == 0:
+            soa_num = soa_num + 1
+        return self.__write_soa_internal(origin_soa, soa_num)
+
 class UpdateSession:
     '''Protocol handling for a single dynamic update request.
 
@@ -189,7 +241,8 @@ class UpdateSession:
 
         '''
         try:
-            self.__get_update_zone()
+            self._get_update_zone()
+            self._create_diff()
             prereq_result = self.__check_prerequisites()
             if prereq_result != Rcode.NOERROR():
                 self.__make_response(prereq_result)
@@ -219,7 +272,7 @@ class UpdateSession:
             self.__make_response(Rcode.SERVFAIL())
             return UPDATE_ERROR, None, None
 
-    def __get_update_zone(self):
+    def _get_update_zone(self):
         '''Parse the zone section and find the zone to be updated.
 
         If the zone section is valid and the specified zone is found in
@@ -228,8 +281,11 @@ class UpdateSession:
                           zone
         __zname: The zone name as a Name object
         __zclass: The zone class as an RRClass object
-        __finder: A ZoneFinder for this zone
-        If this method raises an exception, these members are not set
+        If this method raises an exception, these members are not set.
+
+        Note: This method is protected for ease of use in tests, where
+        methods are tested that need the setup done here without calling
+        the full handle() method.
         '''
         # Validation: the zone section must contain exactly one question,
         # and it must be of type SOA.
@@ -247,10 +303,9 @@ class UpdateSession:
         zclass = zrecord.get_class()
         zone_type, datasrc_client = self.__zone_config.find_zone(zname, zclass)
         if zone_type == isc.ddns.zone_config.ZONE_PRIMARY:
-            _, self.__finder = datasrc_client.find_zone(zname)
+            self.__datasrc_client = datasrc_client
             self.__zname = zname
             self.__zclass = zclass
-            self.__datasrc_client = datasrc_client
             return
         elif zone_type == isc.ddns.zone_config.ZONE_SECONDARY:
             # We are a secondary server; since we don't yet support update
@@ -265,6 +320,26 @@ class UpdateSession:
                      ZoneFormatter(zname, zclass))
         raise UpdateError('notauth', zname, zclass, Rcode.NOTAUTH(), True)
 
+    def _create_diff(self):
+        '''
+        Initializes the internal data structure used for searching current
+        data and for adding and deleting data. This is supposed to be called
+        after ACL checks but before prerequisite checks (since the latter
+        needs the find calls provided by the Diff class).
+        Adds the private member:
+        __diff: A buffer of changes made against the zone by this update
+                This object also contains find() calls, see documentation
+                of the Diff class.
+
+        Note: This method is protected for ease of use in tests, where
+        methods are tested that need the setup done here without calling
+        the full handle() method.
+        '''
+        self.__diff = isc.xfrin.diff.Diff(self.__datasrc_client,
+                                          self.__zname,
+                                          journaling=True,
+                                          single_update_mode=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)
@@ -308,9 +383,7 @@ class UpdateSession:
            only return what the result code would be (and not read/copy
            any actual data).
         '''
-        result, _, _ = self.__finder.find(rrset.get_name(), rrset.get_type(),
-                                          ZoneFinder.NO_WILDCARD |
-                                          ZoneFinder.FIND_GLUE_OK)
+        result, _, _ = self.__diff.find(rrset.get_name(), rrset.get_type())
         return result == ZoneFinder.SUCCESS
 
     def __prereq_rrset_exists_value(self, rrset):
@@ -319,10 +392,8 @@ class UpdateSession:
            RFC2136 Section 2.4.2
            Returns True if the prerequisite is satisfied, False otherwise.
         '''
-        result, found_rrset, _ = self.__finder.find(rrset.get_name(),
-                                                    rrset.get_type(),
-                                                    ZoneFinder.NO_WILDCARD |
-                                                    ZoneFinder.FIND_GLUE_OK)
+        result, found_rrset, _ = self.__diff.find(rrset.get_name(),
+                                                  rrset.get_type())
         if result == ZoneFinder.SUCCESS and\
            rrset.get_name() == found_rrset.get_name() and\
            rrset.get_type() == found_rrset.get_type():
@@ -361,9 +432,7 @@ class UpdateSession:
            to only return what the result code would be (and not read/copy
            any actual data).
         '''
-        result, rrsets, flags = self.__finder.find_all(rrset.get_name(),
-                                                       ZoneFinder.NO_WILDCARD |
-                                                       ZoneFinder.FIND_GLUE_OK)
+        result, rrsets, flags = self.__diff.find_all(rrset.get_name())
         if result == ZoneFinder.SUCCESS and\
            (flags & ZoneFinder.RESULT_WILDCARD == 0):
             return True
@@ -556,20 +625,20 @@ class UpdateSession:
                 return Rcode.FORMERR()
         return Rcode.NOERROR()
 
-    def __do_update_add_single_rr(self, diff, rr, existing_rrset):
+    def __do_update_add_single_rr(self, rr, existing_rrset):
         '''Helper for __do_update_add_rrs_to_rrset: only add the
            rr if it is not present yet
            (note that rr here should already be a single-rr rrset)
         '''
         if existing_rrset is None:
-            diff.add_data(rr)
+            self.__diff.add_data(rr)
         else:
             rr_rdata = rr.get_rdata()[0]
             if not rr_rdata in existing_rrset.get_rdata():
-                diff.add_data(rr)
+                self.__diff.add_data(rr)
 
-    def __do_update_add_rrs_to_rrset(self, diff, rrset):
-        '''Add the rrs from the given rrset to the diff.
+    def __do_update_add_rrs_to_rrset(self, rrset):
+        '''Add the rrs from the given rrset to the internal diff.
            There is handling for a number of special cases mentioned
            in RFC2136;
            - If the addition is a CNAME, but existing data at its
@@ -587,11 +656,9 @@ class UpdateSession:
         # is explicitely ignored here)
         if rrset.get_type() == RRType.SOA():
             return
-        result, orig_rrset, _ = self.__finder.find(rrset.get_name(),
-                                                   rrset.get_type(),
-                                                   ZoneFinder.NO_WILDCARD |
-                                                   ZoneFinder.FIND_GLUE_OK)
-        if result == self.__finder.CNAME:
+        result, orig_rrset, _ = self.__diff.find(rrset.get_name(),
+                                                 rrset.get_type())
+        if result == ZoneFinder.CNAME:
             # Ignore non-cname rrs that try to update CNAME records
             # (if rrset itself is a CNAME, the finder result would be
             # SUCCESS, see next case)
@@ -601,7 +668,7 @@ class UpdateSession:
             if rrset.get_type() == RRType.CNAME():
                 # Remove original CNAME record (the new one
                 # is added below)
-                diff.delete_data(orig_rrset)
+                self.__diff.delete_data(orig_rrset)
             # We do not have WKS support at this time, but if there
             # are special Update equality rules such as for WKS, and
             # we do have support for the type, this is where the check
@@ -612,19 +679,17 @@ class UpdateSession:
             if rrset.get_type() == RRType.CNAME():
                 return
         for rr in foreach_rr(rrset):
-            self.__do_update_add_single_rr(diff, rr, orig_rrset)
+            self.__do_update_add_single_rr(rr, orig_rrset)
 
-    def __do_update_delete_rrset(self, diff, rrset):
+    def __do_update_delete_rrset(self, rrset):
         '''Deletes the rrset with the name and type of the given
            rrset from the zone data (by putting all existing data
-           in the given diff as delete statements).
+           in the internal diff as delete statements).
            Special cases: if the delete statement is for the
            zone's apex, and the type is either SOA or NS, it
            is ignored.'''
-        result, to_delete, _ = self.__finder.find(rrset.get_name(),
-                                                  rrset.get_type(),
-                                                  ZoneFinder.NO_WILDCARD |
-                                                  ZoneFinder.FIND_GLUE_OK)
+        result, to_delete, _ = self.__diff.find(rrset.get_name(),
+                                                rrset.get_type())
         if result == ZoneFinder.SUCCESS:
             if to_delete.get_name() == self.__zname and\
                (to_delete.get_type() == RRType.SOA() or\
@@ -632,9 +697,9 @@ class UpdateSession:
                 # ignore
                 return
             for rr in foreach_rr(to_delete):
-                diff.delete_data(rr)
+                self.__diff.delete_data(rr)
 
-    def __ns_deleter_helper(self, diff, rrset):
+    def __ns_deleter_helper(self, rrset):
         '''Special case helper for deleting NS resource records
            at the zone apex. In that scenario, the last NS record
            may never be removed (and any action that would do so
@@ -646,10 +711,8 @@ class UpdateSession:
         # (see ticket #2016)
         # The related test is currently disabled. When this is fixed,
         # enable that test again.
-        result, orig_rrset, _ = self.__finder.find(rrset.get_name(),
-                                                   rrset.get_type(),
-                                                   ZoneFinder.NO_WILDCARD |
-                                                   ZoneFinder.FIND_GLUE_OK)
+        result, orig_rrset, _ = self.__diff.find(rrset.get_name(),
+                                                 rrset.get_type())
         # Even a real rrset comparison wouldn't help here...
         # The goal is to make sure that after deletion of the
         # given rrset, at least 1 NS record is left (at the apex).
@@ -670,18 +733,16 @@ class UpdateSession:
                                           rrset.get_ttl())
                 to_delete.add_rdata(rdata)
                 orig_rrset_rdata.remove(rdata)
-                diff.delete_data(to_delete)
+                self.__diff.delete_data(to_delete)
 
-    def __do_update_delete_name(self, diff, rrset):
+    def __do_update_delete_name(self, rrset):
         '''Delete all data at the name of the given rrset,
            by adding all data found by find_all as delete statements
-           to the given diff.
+           to the internal diff.
            Special case: if the name is the zone's apex, SOA and
            NS records are kept.
         '''
-        result, rrsets, flags = self.__finder.find_all(rrset.get_name(),
-                                                       ZoneFinder.NO_WILDCARD |
-                                                       ZoneFinder.FIND_GLUE_OK)
+        result, rrsets, flags = self.__diff.find_all(rrset.get_name())
         if result == ZoneFinder.SUCCESS and\
            (flags & ZoneFinder.RESULT_WILDCARD == 0):
             for to_delete in rrsets:
@@ -692,9 +753,9 @@ class UpdateSession:
                     continue
                 else:
                     for rr in foreach_rr(to_delete):
-                        diff.delete_data(rr)
+                        self.__diff.delete_data(rr)
 
-    def __do_update_delete_rrs_from_rrset(self, diff, rrset):
+    def __do_update_delete_rrs_from_rrset(self, rrset):
         '''Deletes all resource records in the given rrset from the
            zone. Resource records that do not exist are ignored.
            If the rrset if of type SOA, it is ignored.
@@ -715,35 +776,40 @@ class UpdateSession:
             elif rrset.get_type() == RRType.NS():
                 # hmm. okay. annoying. There must be at least one left,
                 # delegate to helper method
-                self.__ns_deleter_helper(diff, to_delete)
+                self.__ns_deleter_helper(to_delete)
                 return
         for rr in foreach_rr(to_delete):
-            diff.delete_data(rr)
+            self.__diff.delete_data(rr)
 
-    def __update_soa(self, diff):
+    def __update_soa(self):
         '''Checks the member value __added_soa, and depending on
            whether it has been set and what its value is, creates
            a new SOA if necessary.
            Then removes the original SOA and adds the new one,
-           by adding the needed operations to the given diff.'''
+           by adding the needed operations to the internal diff.'''
         # Get the existing SOA
         # if a new soa was specified, add that one, otherwise, do the
         # serial magic and add the newly created one
 
         # get it from DS and to increment and stuff
-        result, old_soa, _ = self.__finder.find(self.__zname, RRType.SOA(),
-                                                ZoneFinder.NO_WILDCARD |
-                                                ZoneFinder.FIND_GLUE_OK)
-
-        if self.__added_soa is not None:
-            new_soa = self.__added_soa
-            # serial check goes here
+        result, old_soa, _ = self.__diff.find(self.__zname, RRType.SOA(),
+                                              ZoneFinder.NO_WILDCARD |
+                                              ZoneFinder.FIND_GLUE_OK)
+        # We may implement recovering from missing SOA data at some point, but
+        # for now servfail on such a broken state
+        if result != ZoneFinder.SUCCESS:
+            raise UpdateError("Error finding SOA record in datasource.",
+                    self.__zname, self.__zclass, Rcode.SERVFAIL())
+        serial_operation = DDNS_SOA()
+        if self.__added_soa is not None and\
+        serial_operation.soa_update_check(old_soa, self.__added_soa):
+                new_soa = self.__added_soa
         else:
-            new_soa = old_soa
             # increment goes here
+            new_soa = serial_operation.update_soa(old_soa)
 
-        diff.delete_data(old_soa)
-        diff.add_data(new_soa)
+        self.__diff.delete_data(old_soa)
+        self.__diff.add_data(new_soa)
 
     def __do_update(self):
         '''Scan, check, and execute the Update section in the
@@ -758,12 +824,8 @@ class UpdateSession:
 
         # update
         try:
-            # create an ixfr-out-friendly diff structure to work on
-            diff = isc.xfrin.diff.Diff(self.__datasrc_client, self.__zname,
-                                       journaling=True, single_update_mode=True)
-
             # Do special handling for SOA first
-            self.__update_soa(diff)
+            self.__update_soa()
 
             # Algorithm from RFC2136 Section 3.4
             # Note that this works on full rrsets, not individual RRs.
@@ -777,16 +839,16 @@ class UpdateSession:
             # do_update statements)
             for rrset in self.__message.get_section(SECTION_UPDATE):
                 if rrset.get_class() == self.__zclass:
-                    self.__do_update_add_rrs_to_rrset(diff, rrset)
+                    self.__do_update_add_rrs_to_rrset(rrset)
                 elif rrset.get_class() == RRClass.ANY():
                     if rrset.get_type() == RRType.ANY():
-                        self.__do_update_delete_name(diff, rrset)
+                        self.__do_update_delete_name(rrset)
                     else:
-                        self.__do_update_delete_rrset(diff, rrset)
+                        self.__do_update_delete_rrset(rrset)
                 elif rrset.get_class() == RRClass.NONE():
-                    self.__do_update_delete_rrs_from_rrset(diff, rrset)
+                    self.__do_update_delete_rrs_from_rrset(rrset)
 
-            diff.commit()
+            self.__diff.commit()
             return Rcode.NOERROR()
         except isc.datasrc.Error as dse:
             logger.info(LIBDDNS_UPDATE_DATASRC_ERROR, dse)

+ 174 - 6
src/lib/python/isc/ddns/tests/session_tests.py

@@ -94,6 +94,97 @@ def create_rrset(name, rrclass, rrtype, ttl, rdatas = []):
         add_rdata(rrset, rdata)
     return rrset
 
+class SessionModuleTests(unittest.TestCase):
+    '''Tests for module-level functions in the session.py module'''
+
+    def test_foreach_rr_in_rrset(self):
+        rrset = create_rrset("www.example.org", TEST_RRCLASS,
+                             RRType.A(), 3600, [ "192.0.2.1" ])
+
+        l = []
+        for rr in foreach_rr(rrset):
+            l.append(str(rr))
+        self.assertEqual(["www.example.org. 3600 IN A 192.0.2.1\n"], l)
+
+        add_rdata(rrset, "192.0.2.2")
+        add_rdata(rrset, "192.0.2.3")
+
+        # but through the generator, there should be several 1-line entries
+        l = []
+        for rr in foreach_rr(rrset):
+            l.append(str(rr))
+        self.assertEqual(["www.example.org. 3600 IN A 192.0.2.1\n",
+                          "www.example.org. 3600 IN A 192.0.2.2\n",
+                          "www.example.org. 3600 IN A 192.0.2.3\n",
+                         ], l)
+
+    def test_convert_rrset_class(self):
+        # Converting an RRSET to a different class should work
+        # if the rdata types can be converted
+        rrset = create_rrset("www.example.org", RRClass.NONE(), RRType.A(),
+                             3600, [ b'\xc0\x00\x02\x01', b'\xc0\x00\x02\x02'])
+
+        rrset2 = convert_rrset_class(rrset, RRClass.IN())
+        self.assertEqual("www.example.org. 3600 IN A 192.0.2.1\n" +
+                         "www.example.org. 3600 IN A 192.0.2.2\n",
+                         str(rrset2))
+
+        rrset3 = convert_rrset_class(rrset2, RRClass.NONE())
+        self.assertEqual("www.example.org. 3600 CLASS254 A \\# 4 " +
+                         "c0000201\nwww.example.org. 3600 CLASS254 " +
+                         "A \\# 4 c0000202\n",
+                         str(rrset3))
+
+        # depending on what type of bad data is given, a number
+        # of different exceptions could be raised (TODO: i recall
+        # there was a ticket about making a better hierarchy for
+        # dns/parsing related exceptions)
+        self.assertRaises(InvalidRdataLength, convert_rrset_class,
+                          rrset, RRClass.CH())
+        add_rdata(rrset, b'\xc0\x00')
+        self.assertRaises(DNSMessageFORMERR, convert_rrset_class,
+                          rrset, RRClass.IN())
+
+    def test_collect_rrsets(self):
+        '''
+        Tests the 'rrset collector' method, which collects rrsets
+        with the same name and type
+        '''
+        collected = []
+
+        collect_rrsets(collected, create_rrset("a.example.org", RRClass.IN(),
+                                               RRType.A(), 0, [ "192.0.2.1" ]))
+        # Same name and class, different type
+        collect_rrsets(collected, create_rrset("a.example.org", RRClass.IN(),
+                                               RRType.TXT(), 0, [ "one" ]))
+        collect_rrsets(collected, create_rrset("a.example.org", RRClass.IN(),
+                                               RRType.A(), 0, [ "192.0.2.2" ]))
+        collect_rrsets(collected, create_rrset("a.example.org", RRClass.IN(),
+                                               RRType.TXT(), 0, [ "two" ]))
+        # Same class and type as an existing one, different name
+        collect_rrsets(collected, create_rrset("b.example.org", RRClass.IN(),
+                                               RRType.A(), 0, [ "192.0.2.3" ]))
+        # Same name and type as an existing one, different class
+        collect_rrsets(collected, create_rrset("a.example.org", RRClass.CH(),
+                                               RRType.TXT(), 0, [ "one" ]))
+        collect_rrsets(collected, create_rrset("b.example.org", RRClass.IN(),
+                                               RRType.A(), 0, [ "192.0.2.4" ]))
+        collect_rrsets(collected, create_rrset("a.example.org", RRClass.CH(),
+                                               RRType.TXT(), 0, [ "two" ]))
+
+        strings = [ rrset.to_text() for rrset in collected ]
+        # note + vs , in this list
+        expected = ['a.example.org. 0 IN A 192.0.2.1\n' +
+                    'a.example.org. 0 IN A 192.0.2.2\n',
+                    'a.example.org. 0 IN TXT "one"\n' +
+                    'a.example.org. 0 IN TXT "two"\n',
+                    'b.example.org. 0 IN A 192.0.2.3\n' +
+                    'b.example.org. 0 IN A 192.0.2.4\n',
+                    'a.example.org. 0 CH TXT "one"\n' +
+                    'a.example.org. 0 CH TXT "two"\n']
+
+        self.assertEqual(expected, strings)
+
 class SessionTestBase(unittest.TestCase):
     '''Base class for all sesion related tests.
 
@@ -112,7 +203,14 @@ class SessionTestBase(unittest.TestCase):
                                       ZoneConfig([], TEST_RRCLASS,
                                                  self._datasrc_client,
                                                  self._acl_map))
-        self._session._UpdateSession__get_update_zone()
+        self._session._get_update_zone()
+        self._session._create_diff()
+
+    def tearDown(self):
+        # With the Updater created in _get_update_zone, and tests
+        # doing all kinds of crazy stuff, one might get database locked
+        # errors if it doesn't clean up explicitely after each test
+        self._session = None
 
     def check_response(self, msg, expected_rcode):
         '''Perform common checks on update resposne message.'''
@@ -126,6 +224,65 @@ class SessionTestBase(unittest.TestCase):
         self.assertEqual(0, msg.get_rr_count(SECTION_UPDATE))
         self.assertEqual(0, msg.get_rr_count(Message.SECTION_ADDITIONAL))
 
+class TestDDNSSOA(unittest.TestCase):
+    '''unittest for the DDNS_SOA'''
+    def test_update_soa(self):
+        '''unittest for update_soa function'''
+        soa_update = DDNS_SOA()
+        soa_rr = create_rrset("example.org", TEST_RRCLASS,
+                              RRType.SOA(), 3600, ["ns1.example.org. " +
+                              "admin.example.org. " +
+                              "1233 3600 1800 2419200 7200"])
+        expected_soa_rr = create_rrset("example.org", TEST_RRCLASS,
+                                       RRType.SOA(), 3600, ["ns1.example.org. "
+                                       + "admin.example.org. " +
+                                       "1234 3600 1800 2419200 7200"])
+        self.assertEqual(soa_update.update_soa(soa_rr).get_rdata()[0].to_text(),
+                         expected_soa_rr.get_rdata()[0].to_text())
+        max_serial = 2 ** 32 - 1
+        soa_rdata = "%d %s"%(max_serial,"3600 1800 2419200 7200")
+        soa_rr = create_rrset("example.org", TEST_RRCLASS, RRType.SOA(), 3600,
+                              ["ns1.example.org. " + "admin.example.org. " +
+                              soa_rdata])
+        expected_soa_rr = create_rrset("example.org", TEST_RRCLASS,
+                                       RRType.SOA(), 3600, ["ns1.example.org. "
+                                       + "admin.example.org. " +
+                                       "1 3600 1800 2419200 7200"])
+        self.assertEqual(soa_update.update_soa(soa_rr).get_rdata()[0].to_text(),
+                         expected_soa_rr.get_rdata()[0].to_text())
+
+    def test_soa_update_check(self):
+        '''unittest for soa_update_check function'''
+        small_soa_rr = create_rrset("example.org", TEST_RRCLASS, RRType.SOA(),
+                                    3600, ["ns1.example.org. " +
+                                    "admin.example.org. " +
+                                    "1233 3600 1800 2419200 7200"])
+        large_soa_rr = create_rrset("example.org", TEST_RRCLASS, RRType.SOA(),
+                                    3600, ["ns1.example.org. " +
+                                    "admin.example.org. " +
+                                    "1234 3600 1800 2419200 7200"])
+        soa_update = DDNS_SOA()
+        # The case of (i1 < i2 and i2 - i1 < 2^(SERIAL_BITS - 1)) in rfc 1982
+        self.assertTrue(soa_update.soa_update_check(small_soa_rr,
+                                                    large_soa_rr))
+        self.assertFalse(soa_update.soa_update_check(large_soa_rr,
+                                                     small_soa_rr))
+        small_serial = 1235 + 2 ** 31
+        soa_rdata = "%d %s"%(small_serial,"3600 1800 2419200 7200")
+        small_soa_rr = create_rrset("example.org", TEST_RRCLASS, RRType.SOA(),
+                                    3600, ["ns1.example.org. " +
+                                           "admin.example.org. " +
+                                           soa_rdata])
+        large_soa_rr = create_rrset("example.org", TEST_RRCLASS, RRType.SOA(),
+                                    3600, ["ns1.example.org. " +
+                                    "admin.example.org. " +
+                                    "1234 3600 1800 2419200 7200"])
+        # The case of (i1 > i2 and i1 - i2 > 2^(SERIAL_BITS - 1)) in rfc 1982
+        self.assertTrue(soa_update.soa_update_check(small_soa_rr,
+                                                    large_soa_rr))
+        self.assertFalse(soa_update.soa_update_check(large_soa_rr,
+                                                     small_soa_rr))
+
 class SessionTest(SessionTestBase):
     '''Basic session tests'''
 
@@ -463,7 +620,8 @@ class SessionTest(SessionTestBase):
         zconfig = ZoneConfig([], TEST_RRCLASS, self._datasrc_client,
                              self._acl_map)
         session = UpdateSession(msg, TEST_CLIENT4, zconfig)
-        session._UpdateSession__get_update_zone()
+        session._get_update_zone()
+        session._create_diff()
         # compare the to_text output of the rcodes (nicer error messages)
         # This call itself should also be done by handle(),
         # but just for better failures, it is first called on its own
@@ -488,7 +646,8 @@ class SessionTest(SessionTestBase):
         zconfig = ZoneConfig([], TEST_RRCLASS, self._datasrc_client,
                              self._acl_map)
         session = UpdateSession(msg, TEST_CLIENT4, zconfig)
-        session._UpdateSession__get_update_zone()
+        session._get_update_zone()
+        session._create_diff()
         # compare the to_text output of the rcodes (nicer error messages)
         # This call itself should also be done by handle(),
         # but just for better failures, it is first called on its own
@@ -1081,6 +1240,15 @@ class SessionTest(SessionTestBase):
                                       [ "ns1.example.org. " +
                                         "admin.example.org. " +
                                         "1234 3600 1800 2419200 7200" ])
+        # At some point, the SOA SERIAL will be auto-incremented
+        incremented_soa_rrset_01 = create_rrset("example.org", TEST_RRCLASS,
+                RRType.SOA(), 3600, ["ns1.example.org. " +
+                                     "admin.example.org. " +
+                                     "1235 3600 1800 2419200 7200" ])
+        incremented_soa_rrset_02 = create_rrset("example.org", TEST_RRCLASS,
+                RRType.SOA(), 3600, ["ns1.example.org. " +
+                                     "admin.example.org. " +
+                                     "1236 3600 1800 2419200 7200" ])
 
         # We will delete some of the NS records
         orig_ns_rrset = create_rrset("example.org", TEST_RRCLASS,
@@ -1108,16 +1276,16 @@ class SessionTest(SessionTestBase):
         self.__check_inzone_data(isc.datasrc.ZoneFinder.SUCCESS,
                                  isc.dns.Name("example.org"),
                                  RRType.SOA(),
-                                 orig_soa_rrset)
+                                 incremented_soa_rrset_01)
 
         # If we delete everything at the apex, the SOA and NS rrsets should be
-        # untouched
+        # untouched (but serial will be incremented)
         self.check_full_handle_result(Rcode.NOERROR(),
                                       [ self.rrset_update_del_name_apex ])
         self.__check_inzone_data(isc.datasrc.ZoneFinder.SUCCESS,
                                  isc.dns.Name("example.org"),
                                  RRType.SOA(),
-                                 orig_soa_rrset)
+                                 incremented_soa_rrset_02)
         self.__check_inzone_data(isc.datasrc.ZoneFinder.SUCCESS,
                                  isc.dns.Name("example.org"),
                                  RRType.NS(),

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

@@ -1,6 +1,7 @@
 SUBDIRS = tests
 
-python_PYTHON = __init__.py tsig_keyring.py
+python_PYTHON = __init__.py tsig_keyring.py auth_command.py dns_tcp.py
+python_PYTHON += logger.py
 
 pythondir = $(pyexecdir)/isc/server_common
 

+ 90 - 0
src/lib/python/isc/server_common/auth_command.py

@@ -0,0 +1,90 @@
+# Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+'''This module is a utility to create some intermodule command(s) for Auth.'''
+
+from isc.dns import *
+import isc.log
+from isc.config.ccsession import create_command
+from isc.log_messages.server_common_messages import *
+from isc.server_common.logger import logger
+
+AUTH_MODULE_NAME = 'Auth'
+
+def auth_loadzone_command(module_cc, zone_name, zone_class):
+    '''Create a 'loadzone' command with a given zone for Auth server.
+
+    This function checks the Auth module configuration to see if it
+    servers a given zone via an in-memory data source on top of SQLite3
+    data source, and, if so, generate an inter-module command for Auth
+    to force it to reload the zone.
+
+    Parameters:
+    module_cc (CCSession): a CC session that can get access to auth module
+      configuration as a remote configuration
+    zone_name (isc.dns.Name): the zone name to be possibly reloaded
+    zone_class (isc.dns.RRClass): the RR class of the zone to be possibly
+      reloaded.
+
+    Return: a CC command message for the reload if the zone is found;
+      otherwise None.
+
+    '''
+    # Note: this function was originally a dedicated subroutine of xfrin,
+    # but was moved here so it can be shared by some other modules
+    # (specifically, by ddns).  It's expected that we'll soon fundamentally
+    # revisit the whole data source related configuration, at which point
+    # this function should be substantially modified if not completely
+    # deprecated (which is a more likely scenario).  For this reason, the
+    # corresponding tests were still kept in xfrin.
+
+    datasources, is_default =\
+        module_cc.get_remote_config_value(AUTH_MODULE_NAME, "datasources")
+    if is_default:
+        return None
+    for d in datasources:
+        if "type" not in d:
+            continue
+        try:
+            if "class" in d:
+                dclass = RRClass(d["class"])
+            else:
+                dclass = RRClass("IN")
+        except InvalidRRClass as err:
+            logger.info(PYSERVER_COMMON_AUTH_CONFIG_RRCLASS_ERROR, err)
+            continue
+
+        if d["type"].lower() == "memory" and dclass == zone_class:
+            for zone in d["zones"]:
+                if "filetype" not in zone:
+                    continue
+                if "origin" not in zone:
+                    continue
+                if "filetype" not in zone:
+                    continue
+                try:
+                    name = Name(zone["origin"])
+                except (EmptyLabel, TooLongLabel, BadLabelType, BadEscape,
+                        TooLongName, IncompleteName):
+                    logger.info(PYSERVER_COMMON_AUTH_CONFIG_NAME_PARSER_ERROR,
+                                err)
+                    continue
+
+                if zone["filetype"].lower() == "sqlite3" and name == zone_name:
+                    param = {"origin": zone_name.to_text(),
+                             "class": zone_class.to_text(),
+                             "datasrc": d["type"]}
+                    return create_command("loadzone", param)
+    return None

+ 280 - 0
src/lib/python/isc/server_common/dns_tcp.py

@@ -0,0 +1,280 @@
+# Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+#
+# 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.
+
+"""Utility for handling DNS transactions over TCP.
+
+This module defines a few convenient utility classes for handling DNS
+transactions via a TCP socket.
+
+"""
+
+import isc.log
+from isc.server_common.logger import logger
+from isc.log_messages.server_common_messages import *
+from isc.ddns.logger import ClientFormatter
+import errno
+import socket
+import struct
+
+class DNSTCPSendBuffer:
+    '''A composite buffer for a DNS message sent over TCP.
+
+    This class encapsulates binary data supposed to be a complete DNS
+    message, taking into account the 2-byte length field preceeding the
+    actual data.
+
+    An object of this class is constructed with a binary object for the
+    DNS message data (in wire-format), conceptually "appended" to the
+    2-byte length field.  The length is automatically calculated and
+    converted to the wire-format data in the network byte order.
+
+    Its get_data() method returns a binary object corresponding to the
+    consecutive region of the conceptual buffer starting from the specified
+    position.  The returned region may not necessarily contain all remaining
+    data from the specified position; this class can internally hold multiple
+    separate binary objects to represent the conceptual buffer, and,
+    in that case, get_data() identifies the object that contains the
+    specified position of data, and returns the longest consecutive region
+    from that position.  So the caller must call get_data(), incrementing
+    the position as it transmits the data, until it gets None.
+
+    This class is primarily intended to be a private utility for the
+    DNSTCPContext class, but can be used by other general applications
+    that need to send DNS messages over TCP in their own way.
+
+    '''
+    def __init__(self, data):
+        '''Consructor.
+
+        Parameter:
+          data (binary): A binary sequence that is supposed to be a
+            complete DNS message in the wire format.  It must not
+            exceed 65535 bytes in length; otherwise ValueError will be
+            raised.  This class does not check any further validity on
+            the data as a DNS message.
+
+        '''
+        self.__data_size = len(data)
+        self.__len_size = 2     # fixed length
+        if self.__data_size > 0xffff:
+            raise ValueError('Too large data for DNS/TCP, size: ' +
+                             str(self.__data_size))
+        self.__lenbuf = struct.pack('H', socket.htons(self.__data_size))
+        self.__databuf = data
+
+    def get_total_len(self):
+        '''Return the total length of the buffer, including the length field.
+
+        '''
+        return self.__data_size + self.__len_size
+
+    def get_data(self, pos):
+        '''Return a portion of data from a specified position.
+
+        Parameter:
+          pos (int): The position in the TCP DNS message data (including
+          the 2-byte length field) from which the data are to be returned.
+
+        Return:
+          A Python binary object that corresponds to a part of the TCP
+          DNS message data starting at the specified position.  It may
+          or may not contain all remaining data from that position.
+          If the given position is beyond the end of the enrire data,
+          None will be returned.
+
+        '''
+        if pos >= self.__len_size:
+            pos -= self.__len_size
+            if pos >= self.__data_size:
+                return None
+            return self.__databuf[pos:]
+        return self.__lenbuf[pos:]
+
+class DNSTCPContextError(Exception):
+    '''An exception raised against logic errors in DNSTCPContext.
+
+    This is raised only when the context class is used in an unexpected way,
+    that is for a caller's bug.
+
+    '''
+    pass
+
+class DNSTCPContext:
+    '''Context of a TCP connection used for DNS transactions.
+
+    This class offers the following services:
+    - Handle the initial 2-byte length field internally.  The user of
+      this class only has to deal with the bare DNS message (just like
+      the one transmiited over UDP).
+    - Asynchronous I/O.  It supports the non blocking operation mode,
+      where method calls never block.  The caller is told whether it's
+      ongoing and it should watch the socket or it's fully completed.
+    - Socket error handling: it internally catches socket related exceptions
+      and handle them in an appropriate way.  A fatal error will be reported
+      to the caller in the form of a normal return value.  The application
+      of this class can therefore assume it's basically exception free.
+
+    Notes:
+      - the initial implementation only supports non blocking mode, but
+        it's intended to be extended so it can work in both blocking or
+        non blocking mode as we see the need for it.
+      - the initial implementation only supports send operations on an
+        already connected socket, but the intent is to extend this class
+        so it can handle receive or connect operations.
+
+    '''
+
+    # Result codes used in send()/send_ready() methods.
+    SEND_DONE = 1
+    SENDING = 2
+    CLOSED = 3
+
+    def __init__(self, sock):
+        '''Constructor.
+
+        Parameter:
+          sock (Python socket): the socket to be used for the transaction.
+            It must represent a TCP socket; otherwise DNSTCPContextError
+            will be raised.  It's also expected to be connected, but it's
+            not checked on construction; a subsequent send operation would
+            fail.
+
+        '''
+        if sock.proto != socket.IPPROTO_TCP:
+            raise DNSTCPContextError('not a TCP socket, proto: ' +
+                                     str(sock.proto))
+        sock.setblocking(False)
+        self.__sock = sock
+        self.__send_buffer = None
+        self.__remote_addr = sock.getpeername() # record it for logging
+
+    def send(self, data):
+        '''Send a DNS message.
+
+        In the non blocking mode, it sends as much data as possible via
+        the underlying TCP socket until it would block or all data are sent
+        out, and returns the corresponding result code.  This method
+        therefore doesn't block in this mode.
+
+          Note: the initial implementation only works in the non blocking
+          mode.
+
+        This method must not be called once an error is detected and
+        CLOSED is returned or a prior send attempt is ongoing (with
+        the result code of SENDING); otherwise DNSTCPContextError is
+        raised.
+
+        Parameter:
+          data (binary): A binary sequence that is supposed to be a
+            complete DNS message in the wire format.  It must meet
+            the assumption that DNSTCPSendBuffer requires.
+
+        Return:
+          An integer constant representing the result:
+          - SEND_DONE All data have been sent out successfully.
+          - SENDING All writable data has been sent out, and further
+              attempt would block at the moment.  The caller is expected
+              to detect it when the underlying socket is writable again
+              and call send_ready() to continue the send.
+          - CLOSED A network error happened before the send operation is
+              completed.  The underlying socket has been closed, and this
+              context object will be unusable.
+
+        '''
+        if self.__sock is None:
+            raise DNSTCPContextError('send() called after close')
+        if self.__send_buffer is not None:
+            raise DNSTCPContextError('duplicate send()')
+
+        self.__send_buffer = DNSTCPSendBuffer(data)
+        self.__send_marker = 0
+        return self.__do_send()
+
+    def send_ready(self):
+        '''Resume sending a DNS message.
+
+        This method is expected to be called followed by a send() call or
+        another send_ready() call that resulted in SENDING, when the caller
+        detects the underlying socket becomes writable.  It works as
+        send() except that it continues the send operation from the suspended
+        position of the data at the time of the previous call.
+
+        This method must not be called once an error is detected and
+        CLOSED is returned or a send() method hasn't been called to
+        start the operation; otherwise DNSTCPContextError is raised.
+
+        Return: see send().
+
+        '''
+        if self.__sock is None:
+            raise DNSTCPContextError('send() called after close')
+        if self.__send_buffer is None:
+            raise DNSTCPContextError('send_ready() called before send')
+
+        return self.__do_send()
+
+    def __do_send(self):
+        while True:
+            data = self.__send_buffer.get_data(self.__send_marker)
+            if data is None:
+                # send complete; clear the internal buffer for next possible
+                # send.
+                logger.debug(logger.DBGLVL_TRACE_DETAIL,
+                             PYSERVER_COMMON_DNS_TCP_SEND_DONE,
+                             ClientFormatter(self.__remote_addr),
+                             self.__send_marker)
+                self.__send_buffer = None
+                self.__send_marker = 0
+                return self.SEND_DONE
+            try:
+                cc = self.__sock.send(data)
+            except socket.error as ex:
+                total_len = self.__send_buffer.get_total_len()
+                if ex.errno == errno.EAGAIN:
+                    logger.debug(logger.DBGLVL_TRACE_DETAIL,
+                                 PYSERVER_COMMON_DNS_TCP_SEND_PENDING,
+                                 ClientFormatter(self.__remote_addr),
+                                 self.__send_marker, total_len)
+                    return self.SENDING
+                logger.warn(PYSERVER_COMMON_DNS_TCP_SEND_ERROR,
+                            ClientFormatter(self.__remote_addr),
+                            self.__send_marker, total_len, ex)
+                self.__sock.close()
+                self.__sock = None
+                return self.CLOSED
+            self.__send_marker += cc
+
+    def close(self):
+        '''Close the socket.
+
+        This method closes the underlying socket.  Once called, the context
+        object is effectively useless; any further method call would result
+        in a DNSTCPContextError exception.
+
+        The underlying socket will be automatically (and implicitly) closed
+        when this object is deallocated, but Python seems to expect socket
+        objects should be explicitly closed before deallocation.  So it's
+        generally advisable for the user of this object to call this method
+        explictily when it doesn't need the context.
+
+        This method can be called more than once or can be called after
+        other I/O related methods have returned CLOSED; it's compatible
+        with the close() method of the Python socket class.
+
+        '''
+        if self.__sock is None:
+            return
+        self.__sock.close()
+        self.__sock = None      # prevent furhter operation

+ 20 - 0
src/lib/python/isc/server_common/logger.py

@@ -0,0 +1,20 @@
+# Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+#
+# 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.
+
+'''Common definitions regarding logging for the server_common package.'''
+
+import isc.log
+
+logger = isc.log.Logger("server_common")

+ 29 - 0
src/lib/python/isc/server_common/server_common_messages.mes

@@ -21,6 +21,35 @@
 # have that at this moment. So when adding a message, make sure that
 # the name is not already used in src/lib/config/config_messages.mes
 
+% PYSERVER_COMMON_AUTH_CONFIG_NAME_PARSER_ERROR Invalid name when parsing Auth configuration: %1
+There was an invalid name when parsing Auth configuration.
+
+% PYSERVER_COMMON_AUTH_CONFIG_RRCLASS_ERROR Invalid RRClass when parsing Auth configuration: %1
+There was an invalid RR class when parsing Auth configuration.
+
+% PYSERVER_COMMON_DNS_TCP_SEND_DONE completed sending TCP message to %1 (%2 bytes in total)
+Debug message.  A complete DNS message has been successfully
+transmitted over a TCP connection, possibly after multiple send
+operations.  The destination address and the total size of the message
+(including the 2-byte length field) are shown in the log message.
+
+% PYSERVER_COMMON_DNS_TCP_SEND_ERROR failed to send TCP message to %1 (%2/%3 bytes sent): %4
+A DNS message has been attempted to be sent out over a TCP connection,
+but it failed due to some network error.  Although it's not expected
+to happen too often, it can still happen for various reasons.  The
+administrator may want to examine the cause of the failure, which is
+included in the log message, to see if it requires some action to
+be taken at the server side.  When this message is logged, the
+corresponding  TCP connection was closed immediately after the error
+was detected.
+
+% PYSERVER_COMMON_DNS_TCP_SEND_PENDING sent part TCP message to %1 (up to %2/%3 bytes)
+Debug message.  A part of DNS message has been transmitted over a TCP
+connection, and it's suspended because further attempt would block.
+The destination address and the total size of the message that has
+been transmitted so far (including the 2-byte length field) are shown
+in the log message.
+
 % PYSERVER_COMMON_TSIG_KEYRING_DEINIT Deinitializing global TSIG keyring
 A debug message noting that the global TSIG keyring is being removed from
 memory. Most programs don't do that, they just exit, which is OK.

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

@@ -1,5 +1,5 @@
 PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-PYTESTS = tsig_keyring_test.py
+PYTESTS = tsig_keyring_test.py dns_tcp_test.py
 EXTRA_DIST = $(PYTESTS)
 
 # If necessary (rare cases), explicitly specify paths to dynamic libraries

+ 246 - 0
src/lib/python/isc/server_common/tests/dns_tcp_test.py

@@ -0,0 +1,246 @@
+# Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+#
+# 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.
+
+'''Tests for isc.server_common.dns_tcp'''
+
+import isc.log
+from isc.server_common.dns_tcp import *
+import socket
+import errno
+import unittest
+
+def check_length_field(assert_eq, len_data, expected_len):
+    # Examine the "length field" part of the data.  It should be 2-byte field,
+    # and (in our implementation) always given as a separate chunk of data.
+    # The 16-bit length value of the actual data should be stored in the
+    # network byte order.
+    len_high = (expected_len >> 8) & 0x00ff
+    len_low = (expected_len & 0x00ff)
+    assert_eq(2, len(len_data))
+    assert_eq(len_high, len_data[0])
+    assert_eq(len_low, len_data[1])
+
+class BufferTest(unittest.TestCase):
+    def check_length_field(self, buf, expected_len):
+        '''Common subtest for the main tests that checks the length buffer.'''
+        check_length_field(self.assertEqual, buf.get_data(0), expected_len)
+
+        # Confirm the get_data(1) returns the latter half of the (partial)
+        # buffer.
+        self.assertEqual(1, len(buf.get_data(1)))
+        self.assertEqual(expected_len & 0x00ff, buf.get_data(1)[0])
+
+    def test_small_data(self):
+        # The smallest size (in practice) of data: that of the header field.
+        expected_data = b'x' * 12
+        buf = DNSTCPSendBuffer(expected_data)
+        self.check_length_field(buf, 12)
+
+        self.assertEqual(expected_data, buf.get_data(2))
+        self.assertEqual(b'x' * 11, buf.get_data(3))
+        self.assertEqual(None, buf.get_data(14))
+
+    def test_large_data(self):
+        # Test with an arbitrarily large size of data.
+        buf = DNSTCPSendBuffer(b'x' * 65534)
+        self.check_length_field(buf, 65534)
+        self.assertEqual(b'x' * 65534, buf.get_data(2))
+        self.assertEqual(b'x' * 2, buf.get_data(65534))
+        self.assertEqual(None, buf.get_data(65536))
+
+    def test_largest_data(self):
+        # This is the largest possible size of DNS message.
+        buf = DNSTCPSendBuffer(b'y' * 65535)
+        self.check_length_field(buf, 65535)
+        self.assertEqual(b'y', buf.get_data(65536))
+        self.assertEqual(None, buf.get_data(65537))
+
+    def test_too_large_data(self):
+        # The maximum possible size of a valid DNS message is 65535.
+        # Beyond that, the buffer construction should result in an exception.
+        self.assertRaises(ValueError, DNSTCPSendBuffer, b'0' * 65536)
+
+    def test_empty_data(self):
+        # Unusual, but it's not rejected
+        buf = DNSTCPSendBuffer(b'')
+        self.check_length_field(buf, 0)
+        self.assertEqual(None, buf.get_data(2))
+
+    def test_get_total_len(self):
+        self.assertEqual(14, DNSTCPSendBuffer(b'x' * 12).get_total_len())
+        self.assertEqual(2, DNSTCPSendBuffer(b'').get_total_len())
+        self.assertEqual(65537, DNSTCPSendBuffer(b'X' * 65535).get_total_len())
+
+class FakeSocket:
+    '''Emulating python socket w/o involving IO while allowing inspection.'''
+    def __init__(self, proto=socket.IPPROTO_TCP):
+        self._setblocking_val = None # record the latest value of setblocking()
+        self._closed = False         # set to True on close()
+        self._sent_data = []         # record the transmitted data in send()
+        self._send_buflen = None     # capacity of the faked "send buffer";
+                                     # None means infinity, -1 means "closed"
+        self._send_cc = 0            # waterline of the send buffer
+        self.proto = proto # protocol (should be TCP, but can be faked)
+
+    def setblocking(self, on):
+        self._setblocking_val = on
+
+    def close(self):
+        self._closed = True
+
+    def send(self, data):
+        # Calculate the available space in the "send buffer"
+        if self._send_buflen == -1:
+            raise socket.error(errno.EPIPE, "Broken pipe")
+        elif self._send_buflen is None:
+            available_space = len(data)
+        else:
+            available_space = self._send_buflen - self._send_cc
+        if available_space == 0:
+            # if there's no space, (assuming it's nonblocking mode) raise
+            # EAGAIN.
+            raise socket.error(errno.EAGAIN,
+                               "Resource temporarily unavailable")
+        # determine the sendable part of the data, record it, update "buffer".
+        cc = min(available_space, len(data))
+        self._sent_data.append(data[:cc])
+        self._send_cc += cc
+        return cc
+
+    def make_send_ready(self):
+        # pretend that the accrued data has been cleared, making room in
+        # the send buffer.
+        self._send_cc = 0
+
+    def getpeername(self):
+        '''Return faked remote address'''
+        return ('2001:db8::1', 53000, 0, 0)
+
+class ContextTest(unittest.TestCase):
+    def setUp(self):
+        self.__sock = FakeSocket()
+        # there should be no setblocking value on the fake socket by default.
+        self.assertEqual(None, self.__sock._setblocking_val)
+        self.__ctx = DNSTCPContext(self.__sock)
+        # dummy data that has the same length as the DNS header section:
+        self.__test_data = b'x' * 12
+
+    def test_initialization(self):
+        # Creating a context (in setUp) should make the socket non-blocking.
+        self.assertFalse(self.__sock._setblocking_val)
+
+        # Only a TCP socket is accepted.
+        self.assertRaises(DNSTCPContextError, DNSTCPContext,
+                          FakeSocket(proto=socket.IPPROTO_UDP))
+
+    def test_send_all(self):
+        # By default, a single send() call will send out all data by 2
+        # send() calls: one for the 2-byte length data and the other for the
+        # actual data.
+        self.assertEqual(DNSTCPContext.SEND_DONE,
+                         self.__ctx.send(self.__test_data))
+        self.assertEqual(2, len(self.__sock._sent_data))
+        check_length_field(self.assertEqual, self.__sock._sent_data[0],
+                           len(self.__test_data))
+        self.assertEqual(self.__test_data, self.__sock._sent_data[1])
+
+    def test_send_divided(self):
+        # set the "send buffer" of the fake socket to 7 (half of the size of
+        # len + data).
+        self.__sock._send_buflen = 7
+
+        # The initial send() can only send out the half of the data in
+        # two calls to socket.send(): the first one for the length field,
+        # and the other is for the first 5 bytes of the data
+        self.assertEqual(DNSTCPContext.SENDING,
+                         self.__ctx.send(self.__test_data))
+        self.assertEqual(2, len(self.__sock._sent_data))
+        check_length_field(self.assertEqual, self.__sock._sent_data[0],
+                           len(self.__test_data))
+        self.assertEqual(self.__test_data[:5], self.__sock._sent_data[1])
+
+        # "flush" the send buffer of the fake socket
+        self.__sock.make_send_ready()
+
+        # send_ready() can now complete the send.  The remaining data should
+        # have been passed.
+        self.assertEqual(DNSTCPContext.SEND_DONE, self.__ctx.send_ready())
+        self.assertEqual(3, len(self.__sock._sent_data))
+        self.assertEqual(self.__test_data[5:], self.__sock._sent_data[2])
+
+    def test_send_multi(self):
+        # On a successful completion of send, another send can be done.
+        for i in (0, 2):
+            self.assertEqual(DNSTCPContext.SEND_DONE,
+                             self.__ctx.send(self.__test_data))
+            self.assertEqual(i + 2, len(self.__sock._sent_data))
+            check_length_field(self.assertEqual, self.__sock._sent_data[i],
+                               len(self.__test_data))
+            self.assertEqual(self.__test_data, self.__sock._sent_data[i + 1])
+
+    def test_send_reset(self):
+        # the connection will be "reset" before the initial send.
+        # send() should return CLOSED, and the underlying socket should be
+        # closed.
+        self.__sock._send_buflen = -1
+        self.assertEqual(DNSTCPContext.CLOSED,
+                         self.__ctx.send(self.__test_data))
+        self.assertTrue(self.__sock._closed)
+
+        # Once closed, send() cannot be called any more
+        self.assertRaises(DNSTCPContextError, self.__ctx.send,
+                          self.__test_data)
+        # Calling close() is okay (it's NO-OP)
+        self.__ctx.close()
+
+    def test_send_divided_reset(self):
+        # Similar to send_reset, but send() succeeds, and then the connection
+        # will be "reset".
+        self.__sock._send_buflen = 7
+        self.assertEqual(DNSTCPContext.SENDING,
+                         self.__ctx.send(self.__test_data))
+        self.__sock._send_buflen = -1
+        self.assertEqual(DNSTCPContext.CLOSED, self.__ctx.send_ready())
+        self.assertTrue(self.__sock._closed)
+
+        # Once closed, send_ready() cannot be called any more
+        self.assertRaises(DNSTCPContextError, self.__ctx.send_ready)
+
+    def test_duplicate_send(self):
+        # send() cannot be called until it's completed
+        self.__sock._send_buflen = 7
+        self.assertEqual(DNSTCPContext.SENDING,
+                         self.__ctx.send(self.__test_data))
+        self.assertRaises(DNSTCPContextError, self.__ctx.send,
+                          self.__test_data)
+
+    def test_skip_send(self):
+        # send_ready() cannot be called before send().
+        self.assertRaises(DNSTCPContextError, self.__ctx.send_ready)
+
+    def test_close(self):
+        self.assertEqual(DNSTCPContext.SEND_DONE,
+                         self.__ctx.send(self.__test_data))
+        self.__ctx.close()
+        self.assertTrue(self.__sock._closed)
+
+        # Duplicate close is just ignored, and the socket is still closed.
+        self.__ctx.close()
+        self.assertTrue(self.__sock._closed)
+
+if __name__ == "__main__":
+    isc.log.init("bind10")
+    isc.log.resetUnitTestRootLogger()
+    unittest.main()

+ 1 - 1
src/lib/python/isc/server_common/tsig_keyring.py

@@ -20,10 +20,10 @@ tsig_keys module.
 
 import isc.dns
 import isc.log
+from isc.server_common.logger import logger
 from isc.log_messages.server_common_messages import *
 
 updater = None
-logger = isc.log.Logger("server_common")
 
 class Unexpected(Exception):
     """

+ 35 - 4
src/lib/python/isc/xfrin/diff.py

@@ -25,6 +25,7 @@ But for now, it lives here.
 
 import isc.dns
 import isc.log
+from isc.datasrc import ZoneFinder
 from isc.log_messages.libxfrin_messages import *
 
 class NoSuchZone(Exception):
@@ -119,7 +120,7 @@ class Diff:
         else:
             self.__buffer = []
 
-    def __check_commited(self):
+    def __check_committed(self):
         """
         This checks if the diff is already commited or broken. If it is, it
         raises ValueError. This check is for methods that need to work only on
@@ -169,7 +170,7 @@ class Diff:
         - in single_update_mode if any later rr is of type SOA (both for
           addition and deletion)
         """
-        self.__check_commited()
+        self.__check_committed()
         if rr.get_rdata_count() != 1:
             raise ValueError('The rrset must contain exactly 1 Rdata, but ' +
                              'it holds ' + str(rr.get_rdata_count()))
@@ -298,7 +299,7 @@ class Diff:
                 else:
                     raise ValueError('Unknown operation ' + operation)
 
-        self.__check_commited()
+        self.__check_committed()
         # First, compact the data
         self.compact()
         try:
@@ -330,7 +331,7 @@ class Diff:
 
         This might raise isc.datasrc.Error.
         """
-        self.__check_commited()
+        self.__check_committed()
         # Push the data inside the data source
         self.apply()
         # Make sure they are visible.
@@ -376,3 +377,33 @@ class Diff:
             raise ValueError("Separate buffers requested in single-update mode")
         else:
             return (self.__deletions, self.__additions)
+
+    def find(self, name, rrtype,
+             options=(ZoneFinder.NO_WILDCARD | ZoneFinder.FIND_GLUE_OK)):
+        """
+        Calls the find() method in the ZoneFinder associated with this
+        Diff's ZoneUpdater, i.e. the find() on the zone as it was on the
+        moment this Diff object got created.
+        See the ZoneFinder documentation for a full description.
+        Note that the result does not include changes made in this Diff
+        instance so far.
+        Options default to NO_WILDCARD and FIND_GLUE_OK.
+        Raises a ValueError if the Diff has been committed already
+        """
+        self.__check_committed()
+        return self.__updater.find(name, rrtype, options)
+
+    def find_all(self, name,
+                 options=(ZoneFinder.NO_WILDCARD | ZoneFinder.FIND_GLUE_OK)):
+        """
+        Calls the find() method in the ZoneFinder associated with this
+        Diff's ZoneUpdater, i.e. the find_all() on the zone as it was on the
+        moment this Diff object got created.
+        See the ZoneFinder documentation for a full description.
+        Note that the result does not include changes made in this Diff
+        instance so far.
+        Options default to NO_WILDCARD and FIND_GLUE_OK.
+        Raises a ValueError if the Diff has been committed already
+        """
+        self.__check_committed()
+        return self.__updater.find_all(name, options)

+ 88 - 1
src/lib/python/isc/xfrin/tests/diff_tests.py

@@ -15,7 +15,7 @@
 
 import isc.log
 import unittest
-import isc.datasrc
+from isc.datasrc import ZoneFinder
 from isc.dns import Name, RRset, RRClass, RRType, RRTTL, Rdata
 from isc.xfrin.diff import Diff, NoSuchZone
 
@@ -48,6 +48,13 @@ class DiffTest(unittest.TestCase):
         self.__broken_called = False
         self.__warn_called = False
         self.__should_replace = False
+        self.__find_called = False
+        self.__find_name = None
+        self.__find_type = None
+        self.__find_options = None
+        self.__find_all_called = False
+        self.__find_all_name = None
+        self.__find_all_options = None
         # Some common values
         self.__rrclass = RRClass.IN()
         self.__type = RRType.A()
@@ -156,6 +163,23 @@ class DiffTest(unittest.TestCase):
 
         return self
 
+    def find(self, name, rrtype, options=None):
+        self.__find_called = True
+        self.__find_name = name
+        self.__find_type = rrtype
+        self.__find_options = options
+        # Doesn't really matter what is returned, as long
+        # as the test can check that it's passed along
+        return "find_return"
+
+    def find_all(self, name, options=None):
+        self.__find_all_called = True
+        self.__find_all_name = name
+        self.__find_all_options = options
+        # Doesn't really matter what is returned, as long
+        # as the test can check that it's passed along
+        return "find_all_return"
+
     def test_create(self):
         """
         This test the case when the diff is successfuly created. It just
@@ -265,6 +289,9 @@ class DiffTest(unittest.TestCase):
         self.assertRaises(ValueError, diff.commit)
         self.assertRaises(ValueError, diff.add_data, self.__rrset2)
         self.assertRaises(ValueError, diff.delete_data, self.__rrset1)
+        self.assertRaises(ValueError, diff.find, Name('foo.example.org.'),
+                          RRType.A())
+        self.assertRaises(ValueError, diff.find_all, Name('foo.example.org.'))
         diff.apply = orig_apply
         self.assertRaises(ValueError, diff.apply)
         # This one does not state it should raise, so check it doesn't
@@ -587,6 +614,66 @@ class DiffTest(unittest.TestCase):
         self.assertRaises(ValueError, diff.add_data, a)
         self.assertRaises(ValueError, diff.delete_data, a)
 
+    def test_find(self):
+        diff = Diff(self, Name('example.org.'))
+        name = Name('www.example.org.')
+        rrtype = RRType.A()
+
+        self.assertFalse(self.__find_called)
+        self.assertEqual(None, self.__find_name)
+        self.assertEqual(None, self.__find_type)
+        self.assertEqual(None, self.__find_options)
+
+        self.assertEqual("find_return", diff.find(name, rrtype))
+
+        self.assertTrue(self.__find_called)
+        self.assertEqual(name, self.__find_name)
+        self.assertEqual(rrtype, self.__find_type)
+        self.assertEqual(ZoneFinder.NO_WILDCARD | ZoneFinder.FIND_GLUE_OK,
+                         self.__find_options)
+
+    def test_find_options(self):
+        diff = Diff(self, Name('example.org.'))
+        name = Name('foo.example.org.')
+        rrtype = RRType.TXT()
+        options = ZoneFinder.NO_WILDCARD
+
+        self.assertEqual("find_return", diff.find(name, rrtype, options))
+
+        self.assertTrue(self.__find_called)
+        self.assertEqual(name, self.__find_name)
+        self.assertEqual(rrtype, self.__find_type)
+        self.assertEqual(options, self.__find_options)
+
+    def test_find_all(self):
+        diff = Diff(self, Name('example.org.'))
+        name = Name('www.example.org.')
+
+        self.assertFalse(self.__find_all_called)
+        self.assertEqual(None, self.__find_all_name)
+        self.assertEqual(None, self.__find_all_options)
+
+        self.assertEqual("find_all_return", diff.find_all(name))
+
+        self.assertTrue(self.__find_all_called)
+        self.assertEqual(name, self.__find_all_name)
+        self.assertEqual(ZoneFinder.NO_WILDCARD | ZoneFinder.FIND_GLUE_OK,
+                         self.__find_all_options)
+
+    def test_find_all_options(self):
+        diff = Diff(self, Name('example.org.'))
+        name = Name('www.example.org.')
+        options = isc.datasrc.ZoneFinder.NO_WILDCARD
+
+        self.assertFalse(self.__find_all_called)
+        self.assertEqual(None, self.__find_all_name)
+        self.assertEqual(None, self.__find_all_options)
+
+        self.assertEqual("find_all_return", diff.find_all(name, options))
+
+        self.assertTrue(self.__find_all_called)
+        self.assertEqual(name, self.__find_all_name)
+        self.assertEqual(options, self.__find_all_options)
 
 if __name__ == "__main__":
     isc.log.init("bind10")