Browse Source

[master] Merge branch 'trac1209' with resolving a minor conflict.

JINMEI Tatuya 13 years ago
parent
commit
5ca7b409bc

+ 13 - 6
doc/guide/bind10-guide.xml

@@ -1272,6 +1272,19 @@ TODO
       and care should be taken to enable IXFR.
       and care should be taken to enable IXFR.
     </para>
     </para>
 
 
+    <note><simpara>
+     In the current development release of BIND 10, incoming zone
+     transfers are only available for SQLite3-based data sources,
+     that is, they don't work for an in-memory data source.
+     Furthermore, the corresponding SQLite3 database must be
+     configured with a list of zone names by hand.  One possible way
+     to do this is to use the <command>b10-loadzone</command> command
+     to load dummy zone content of the zone for which the secondary
+     service is provided (and then force transfer using AXFR from the primary
+     server).  In future versions we will provide more convenient way
+     to set up the secondary.
+    </simpara></note>
+
     <para>
     <para>
       To enable IXFR, you need to
       To enable IXFR, you need to
       configure <command>b10-xfrin</command> with an explicit zone
       configure <command>b10-xfrin</command> with an explicit zone
@@ -1311,12 +1324,6 @@ TODO
       version, at which point we will enable IXFR by default.
       version, at which point we will enable IXFR by default.
     </para>
     </para>
 
 
-    <note><simpara>
-     In the current development release of BIND 10, incoming zone
-     transfers are only available for SQLite3-based data sources,
-     that is, they don't work for an in-memory data source.
-    </simpara></note>
-
 <!-- TODO:
 <!-- TODO:
 
 
 how to tell bind10 you are a secondary?
 how to tell bind10 you are a secondary?

+ 259 - 75
src/bin/xfrin/tests/xfrin_test.py

@@ -93,6 +93,9 @@ def check_diffs(assert_fn, expected, actual):
 class XfrinTestException(Exception):
 class XfrinTestException(Exception):
     pass
     pass
 
 
+class XfrinTestTimeoutException(Exception):
+    pass
+
 class MockCC():
 class MockCC():
     def get_default_value(self, identifier):
     def get_default_value(self, identifier):
         if identifier == "zones/master_port":
         if identifier == "zones/master_port":
@@ -109,6 +112,7 @@ class MockDataSourceClient():
 
 
     '''
     '''
     def __init__(self):
     def __init__(self):
+        self.force_fail = False # if True, raise an exception on commit
         self.committed_diffs = []
         self.committed_diffs = []
         self.diffs = []
         self.diffs = []
 
 
@@ -169,6 +173,8 @@ class MockDataSourceClient():
         self.diffs.append(('delete', rrset))
         self.diffs.append(('delete', rrset))
 
 
     def commit(self):
     def commit(self):
+        if self.force_fail:
+            raise isc.datasrc.Error('Updater.commit() failed')
         self.committed_diffs.append(self.diffs)
         self.committed_diffs.append(self.diffs)
         self.diffs = []
         self.diffs = []
 
 
@@ -205,10 +211,10 @@ class MockXfrin(Xfrin):
                                  request_type, check_soa)
                                  request_type, check_soa)
 
 
 class MockXfrinConnection(XfrinConnection):
 class MockXfrinConnection(XfrinConnection):
-    def __init__(self, sock_map, zone_name, rrclass, db_file, shutdown_event,
+    def __init__(self, sock_map, zone_name, rrclass, shutdown_event,
                  master_addr):
                  master_addr):
         super().__init__(sock_map, zone_name, rrclass, MockDataSourceClient(),
         super().__init__(sock_map, zone_name, rrclass, MockDataSourceClient(),
-                         db_file, shutdown_event, master_addr)
+                         shutdown_event, master_addr)
         self.query_data = b''
         self.query_data = b''
         self.reply_data = b''
         self.reply_data = b''
         self.force_time_out = False
         self.force_time_out = False
@@ -229,6 +235,8 @@ class MockXfrinConnection(XfrinConnection):
     def recv(self, size):
     def recv(self, size):
         data = self.reply_data[:size]
         data = self.reply_data[:size]
         self.reply_data = self.reply_data[size:]
         self.reply_data = self.reply_data[size:]
+        if len(data) == 0:
+            raise XfrinTestTimeoutException('Emulated timeout')
         if len(data) < size:
         if len(data) < size:
             raise XfrinTestException('cannot get reply data (' + str(size) +
             raise XfrinTestException('cannot get reply data (' + str(size) +
                                      ' bytes)')
                                      ' bytes)')
@@ -287,8 +295,7 @@ class TestXfrinState(unittest.TestCase):
     def setUp(self):
     def setUp(self):
         self.sock_map = {}
         self.sock_map = {}
         self.conn = MockXfrinConnection(self.sock_map, TEST_ZONE_NAME,
         self.conn = MockXfrinConnection(self.sock_map, TEST_ZONE_NAME,
-                                        TEST_RRCLASS, TEST_DB_FILE,
+                                        TEST_RRCLASS, threading.Event(),
-                                        threading.Event(),
                                         TEST_MASTER_IPV4_ADDRINFO)
                                         TEST_MASTER_IPV4_ADDRINFO)
         self.begin_soa = RRset(TEST_ZONE_NAME, TEST_RRCLASS, RRType.SOA(),
         self.begin_soa = RRset(TEST_ZONE_NAME, TEST_RRCLASS, RRType.SOA(),
                                RRTTL(3600))
                                RRTTL(3600))
@@ -505,6 +512,7 @@ class TestXfrinAXFR(TestXfrinState):
     def setUp(self):
     def setUp(self):
         super().setUp()
         super().setUp()
         self.state = XfrinAXFR()
         self.state = XfrinAXFR()
+        self.conn._end_serial = 1234
 
 
     def test_handle_rr(self):
     def test_handle_rr(self):
         """
         """
@@ -524,6 +532,13 @@ class TestXfrinAXFR(TestXfrinState):
         self.assertEqual([('add', self.a_rrset), ('add', soa_rrset)],
         self.assertEqual([('add', self.a_rrset), ('add', soa_rrset)],
                          self.conn._diff.get_buffer())
                          self.conn._diff.get_buffer())
 
 
+    def test_handle_rr_mismatch_soa(self):
+        """ SOA with inconsistent serial - unexpected, but we accept it.
+
+        """
+        self.assertTrue(self.state.handle_rr(self.conn, begin_soa_rrset))
+        self.assertEqual(type(XfrinAXFREnd()), type(self.conn.get_xfrstate()))
+
     def test_finish_message(self):
     def test_finish_message(self):
         """
         """
         Check normal end of message.
         Check normal end of message.
@@ -565,8 +580,7 @@ class TestXfrinConnection(unittest.TestCase):
             os.remove(TEST_DB_FILE)
             os.remove(TEST_DB_FILE)
         self.sock_map = {}
         self.sock_map = {}
         self.conn = MockXfrinConnection(self.sock_map, TEST_ZONE_NAME,
         self.conn = MockXfrinConnection(self.sock_map, TEST_ZONE_NAME,
-                                        TEST_RRCLASS, TEST_DB_FILE,
+                                        TEST_RRCLASS, threading.Event(),
-                                        threading.Event(),
                                         TEST_MASTER_IPV4_ADDRINFO)
                                         TEST_MASTER_IPV4_ADDRINFO)
         self.soa_response_params = {
         self.soa_response_params = {
             'questions': [example_soa_question],
             'questions': [example_soa_question],
@@ -577,6 +591,10 @@ class TestXfrinConnection(unittest.TestCase):
             'axfr_after_soa': self._create_normal_response_data
             'axfr_after_soa': self._create_normal_response_data
             }
             }
         self.axfr_response_params = {
         self.axfr_response_params = {
+            'question_1st': default_questions,
+            'question_2nd': default_questions,
+            'answer_1st': [soa_rrset, self._create_ns()],
+            'answer_2nd': default_answers,
             'tsig_1st': None,
             'tsig_1st': None,
             'tsig_2nd': None
             'tsig_2nd': None
             }
             }
@@ -586,24 +604,23 @@ class TestXfrinConnection(unittest.TestCase):
         if os.path.exists(TEST_DB_FILE):
         if os.path.exists(TEST_DB_FILE):
             os.remove(TEST_DB_FILE)
             os.remove(TEST_DB_FILE)
 
 
-    def _handle_xfrin_response(self):
-        # This helper methods iterates over all RRs (excluding the ending SOA)
-        # transferred, and simply returns the number of RRs.  The return value
-        # may be used an assertion value for test cases.
-        rrs = 0
-        for rr in self.conn._handle_axfrin_response():
-            rrs += 1
-        return rrs
-
     def _create_normal_response_data(self):
     def _create_normal_response_data(self):
         # This helper method creates a simple sequence of DNS messages that
         # This helper method creates a simple sequence of DNS messages that
-        # forms a valid AXFR transaction.  It consists of two messages, each
+        # forms a valid AXFR transaction.  It consists of two messages: the
-        # containing just a single SOA RR.
+        # first one containing SOA, NS, the second containing the trailing SOA.
+        question_1st = self.axfr_response_params['question_1st']
+        question_2nd = self.axfr_response_params['question_2nd']
+        answer_1st = self.axfr_response_params['answer_1st']
+        answer_2nd = self.axfr_response_params['answer_2nd']
         tsig_1st = self.axfr_response_params['tsig_1st']
         tsig_1st = self.axfr_response_params['tsig_1st']
         tsig_2nd = self.axfr_response_params['tsig_2nd']
         tsig_2nd = self.axfr_response_params['tsig_2nd']
-        self.conn.reply_data = self.conn.create_response_data(tsig_ctx=tsig_1st)
+        self.conn.reply_data = self.conn.create_response_data(
+            questions=question_1st, answers=answer_1st,
+            tsig_ctx=tsig_1st)
         self.conn.reply_data += \
         self.conn.reply_data += \
-            self.conn.create_response_data(tsig_ctx=tsig_2nd)
+            self.conn.create_response_data(questions=question_2nd,
+                                           answers=answer_2nd,
+                                           tsig_ctx=tsig_2nd)
 
 
     def _create_soa_response_data(self):
     def _create_soa_response_data(self):
         # This helper method creates a DNS message that is supposed to be
         # This helper method creates a DNS message that is supposed to be
@@ -661,6 +678,7 @@ class TestXfrinConnection(unittest.TestCase):
 class TestAXFR(TestXfrinConnection):
 class TestAXFR(TestXfrinConnection):
     def setUp(self):
     def setUp(self):
         super().setUp()
         super().setUp()
+        XfrinInitialSOA().set_xfrstate(self.conn, XfrinInitialSOA())
 
 
     def __create_mock_tsig(self, key, error):
     def __create_mock_tsig(self, key, error):
         # This helper function creates a MockTSIGContext for a given key
         # This helper function creates a MockTSIGContext for a given key
@@ -697,14 +715,13 @@ class TestAXFR(TestXfrinConnection):
         # to confirm an AF_INET6 socket has been created.  A naive application
         # to confirm an AF_INET6 socket has been created.  A naive application
         # tends to assume it's IPv4 only and hardcode AF_INET.  This test
         # tends to assume it's IPv4 only and hardcode AF_INET.  This test
         # uncovers such a bug.
         # uncovers such a bug.
-        c = MockXfrinConnection({}, TEST_ZONE_NAME, TEST_RRCLASS, TEST_DB_FILE,
+        c = MockXfrinConnection({}, TEST_ZONE_NAME, TEST_RRCLASS,
-                                threading.Event(),
+                                threading.Event(), TEST_MASTER_IPV6_ADDRINFO)
-                                TEST_MASTER_IPV6_ADDRINFO)
         c.bind(('::', 0))
         c.bind(('::', 0))
         c.close()
         c.close()
 
 
     def test_init_chclass(self):
     def test_init_chclass(self):
-        c = MockXfrinConnection({}, TEST_ZONE_NAME, RRClass.CH(), TEST_DB_FILE,
+        c = MockXfrinConnection({}, TEST_ZONE_NAME, RRClass.CH(),
                                 threading.Event(), TEST_MASTER_IPV4_ADDRINFO)
                                 threading.Event(), TEST_MASTER_IPV4_ADDRINFO)
         axfrmsg = c._create_query(RRType.AXFR())
         axfrmsg = c._create_query(RRType.AXFR())
         self.assertEqual(axfrmsg.get_question()[0].get_class(),
         self.assertEqual(axfrmsg.get_question()[0].get_class(),
@@ -792,24 +809,28 @@ class TestAXFR(TestXfrinConnection):
 
 
     def test_response_with_invalid_msg(self):
     def test_response_with_invalid_msg(self):
         self.conn.reply_data = b'aaaxxxx'
         self.conn.reply_data = b'aaaxxxx'
-        self.assertRaises(XfrinTestException, self._handle_xfrin_response)
+        self.assertRaises(XfrinTestException,
+                          self.conn._handle_xfrin_responses)
 
 
     def test_response_with_tsigfail(self):
     def test_response_with_tsigfail(self):
         self.conn._tsig_key = TSIG_KEY
         self.conn._tsig_key = TSIG_KEY
         # server tsig check fail, return with RCODE 9 (NOTAUTH)
         # server tsig check fail, return with RCODE 9 (NOTAUTH)
         self.conn._send_query(RRType.SOA())
         self.conn._send_query(RRType.SOA())
         self.conn.reply_data = self.conn.create_response_data(rcode=Rcode.NOTAUTH())
         self.conn.reply_data = self.conn.create_response_data(rcode=Rcode.NOTAUTH())
-        self.assertRaises(XfrinException, self._handle_xfrin_response)
+        self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
 
 
     def test_response_without_end_soa(self):
     def test_response_without_end_soa(self):
         self.conn._send_query(RRType.AXFR())
         self.conn._send_query(RRType.AXFR())
         self.conn.reply_data = self.conn.create_response_data()
         self.conn.reply_data = self.conn.create_response_data()
-        self.assertRaises(XfrinTestException, self._handle_xfrin_response)
+        # This should result in timeout in the asyncore loop.  We emulate
+        # that situation in recv() by emptying the reply data buffer.
+        self.assertRaises(XfrinTestTimeoutException,
+                          self.conn._handle_xfrin_responses)
 
 
     def test_response_bad_qid(self):
     def test_response_bad_qid(self):
         self.conn._send_query(RRType.AXFR())
         self.conn._send_query(RRType.AXFR())
-        self.conn.reply_data = self.conn.create_response_data(bad_qid = True)
+        self.conn.reply_data = self.conn.create_response_data(bad_qid=True)
-        self.assertRaises(XfrinException, self._handle_xfrin_response)
+        self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
 
 
     def test_response_error_code_bad_sig(self):
     def test_response_error_code_bad_sig(self):
         self.conn._tsig_key = TSIG_KEY
         self.conn._tsig_key = TSIG_KEY
@@ -822,7 +843,7 @@ class TestAXFR(TestXfrinConnection):
         # validate log message for XfrinException
         # validate log message for XfrinException
         self.__match_exception(XfrinException,
         self.__match_exception(XfrinException,
                                "TSIG verify fail: BADSIG",
                                "TSIG verify fail: BADSIG",
-                               self._handle_xfrin_response)
+                               self.conn._handle_xfrin_responses)
 
 
     def test_response_bad_qid_bad_key(self):
     def test_response_bad_qid_bad_key(self):
         self.conn._tsig_key = TSIG_KEY
         self.conn._tsig_key = TSIG_KEY
@@ -834,36 +855,29 @@ class TestAXFR(TestXfrinConnection):
         # validate log message for XfrinException
         # validate log message for XfrinException
         self.__match_exception(XfrinException,
         self.__match_exception(XfrinException,
                                "TSIG verify fail: BADKEY",
                                "TSIG verify fail: BADKEY",
-                               self._handle_xfrin_response)
+                               self.conn._handle_xfrin_responses)
 
 
     def test_response_non_response(self):
     def test_response_non_response(self):
         self.conn._send_query(RRType.AXFR())
         self.conn._send_query(RRType.AXFR())
-        self.conn.reply_data = self.conn.create_response_data(response = False)
+        self.conn.reply_data = self.conn.create_response_data(response=False)
-        self.assertRaises(XfrinException, self._handle_xfrin_response)
+        self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
 
 
     def test_response_error_code(self):
     def test_response_error_code(self):
         self.conn._send_query(RRType.AXFR())
         self.conn._send_query(RRType.AXFR())
         self.conn.reply_data = self.conn.create_response_data(
         self.conn.reply_data = self.conn.create_response_data(
             rcode=Rcode.SERVFAIL())
             rcode=Rcode.SERVFAIL())
-        self.assertRaises(XfrinException, self._handle_xfrin_response)
+        self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
 
 
     def test_response_multi_question(self):
     def test_response_multi_question(self):
         self.conn._send_query(RRType.AXFR())
         self.conn._send_query(RRType.AXFR())
         self.conn.reply_data = self.conn.create_response_data(
         self.conn.reply_data = self.conn.create_response_data(
             questions=[example_axfr_question, example_axfr_question])
             questions=[example_axfr_question, example_axfr_question])
-        self.assertRaises(XfrinException, self._handle_xfrin_response)
+        self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
-
-    def test_response_empty_answer(self):
-        self.conn._send_query(RRType.AXFR())
-        self.conn.reply_data = self.conn.create_response_data(answers=[])
-        # Should an empty answer trigger an exception?  Even though it's very
-        # unusual it's not necessarily invalid.  Need to revisit.
-        self.assertRaises(XfrinException, self._handle_xfrin_response)
 
 
     def test_response_non_response(self):
     def test_response_non_response(self):
         self.conn._send_query(RRType.AXFR())
         self.conn._send_query(RRType.AXFR())
         self.conn.reply_data = self.conn.create_response_data(response = False)
         self.conn.reply_data = self.conn.create_response_data(response = False)
-        self.assertRaises(XfrinException, self._handle_xfrin_response)
+        self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
 
 
     def test_soacheck(self):
     def test_soacheck(self):
         # we need to defer the creation until we know the QID, which is
         # we need to defer the creation until we know the QID, which is
@@ -954,30 +968,155 @@ class TestAXFR(TestXfrinConnection):
         self.conn.response_generator = self._create_normal_response_data
         self.conn.response_generator = self._create_normal_response_data
         self.conn._shutdown_event.set()
         self.conn._shutdown_event.set()
         self.conn._send_query(RRType.AXFR())
         self.conn._send_query(RRType.AXFR())
-        self.assertRaises(XfrinException, self._handle_xfrin_response)
+        self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
 
 
     def test_response_timeout(self):
     def test_response_timeout(self):
         self.conn.response_generator = self._create_normal_response_data
         self.conn.response_generator = self._create_normal_response_data
         self.conn.force_time_out = True
         self.conn.force_time_out = True
-        self.assertRaises(XfrinException, self._handle_xfrin_response)
+        self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
 
 
     def test_response_remote_close(self):
     def test_response_remote_close(self):
         self.conn.response_generator = self._create_normal_response_data
         self.conn.response_generator = self._create_normal_response_data
         self.conn.force_close = True
         self.conn.force_close = True
-        self.assertRaises(XfrinException, self._handle_xfrin_response)
+        self.assertRaises(XfrinException, self.conn._handle_xfrin_responses)
 
 
     def test_response_bad_message(self):
     def test_response_bad_message(self):
         self.conn.response_generator = self._create_broken_response_data
         self.conn.response_generator = self._create_broken_response_data
         self.conn._send_query(RRType.AXFR())
         self.conn._send_query(RRType.AXFR())
-        self.assertRaises(Exception, self._handle_xfrin_response)
+        self.assertRaises(Exception, self.conn._handle_xfrin_responses)
 
 
     def test_axfr_response(self):
     def test_axfr_response(self):
-        # normal case.
+        # A simple normal case: AXFR consists of SOA, NS, then trailing SOA.
+        self.conn.response_generator = self._create_normal_response_data
+        self.conn._send_query(RRType.AXFR())
+        self.conn._handle_xfrin_responses()
+        self.assertEqual(type(XfrinAXFREnd()), type(self.conn.get_xfrstate()))
+        check_diffs(self.assertEqual,
+                    [[('add', self._create_ns()), ('add', soa_rrset)]],
+                    self.conn._datasrc_client.committed_diffs)
+
+    def test_response_empty_answer(self):
+        '''Test with an empty AXFR answer section.
+
+        This is an unusual response, but there is no reason to reject it.
+        The second message is a complete AXFR response, and transfer should
+        succeed just like the normal case.
+
+        '''
+
+        self.axfr_response_params['answer_1st'] = []
+        self.axfr_response_params['answer_2nd'] = [soa_rrset,
+                                                   self._create_ns(),
+                                                   soa_rrset]
+        self.conn.response_generator = self._create_normal_response_data
+        self.conn._send_query(RRType.AXFR())
+        self.conn._handle_xfrin_responses()
+        self.assertEqual(type(XfrinAXFREnd()), type(self.conn.get_xfrstate()))
+        check_diffs(self.assertEqual,
+                    [[('add', self._create_ns()), ('add', soa_rrset)]],
+                    self.conn._datasrc_client.committed_diffs)
+
+    def test_axfr_response_soa_mismatch(self):
+        '''AXFR response whose begin/end SOAs are not same.
+
+        What should we do this is moot, for now we accept it, so does BIND 9.
+
+        '''
+        ns_rr = self._create_ns()
+        a_rr = self._create_a('192.0.2.1')
+        self.conn._send_query(RRType.AXFR())
+        self.conn.reply_data = self.conn.create_response_data(
+            questions=[Question(TEST_ZONE_NAME, TEST_RRCLASS,
+                                RRType.AXFR())],
+            # begin serial=1230, end serial=1234. end will be used.
+            answers=[begin_soa_rrset, ns_rr, a_rr, soa_rrset])
+        self.conn._handle_xfrin_responses()
+        self.assertEqual(type(XfrinAXFREnd()), type(self.conn.get_xfrstate()))
+        check_diffs(self.assertEqual,
+                    [[('add', ns_rr), ('add', a_rr), ('add', soa_rrset)]],
+                    self.conn._datasrc_client.committed_diffs)
+
+    def test_axfr_response_extra(self):
+        '''Test with an extra RR after the end of AXFR session.
+
+        The session should be rejected, and nothing should be committed.
+
+        '''
+        ns_rr = self._create_ns()
+        a_rr = self._create_a('192.0.2.1')
+        self.conn._send_query(RRType.AXFR())
+        self.conn.reply_data = self.conn.create_response_data(
+            questions=[Question(TEST_ZONE_NAME, TEST_RRCLASS,
+                                RRType.AXFR())],
+            answers=[soa_rrset, ns_rr, a_rr, soa_rrset, a_rr])
+        self.assertRaises(XfrinProtocolError,
+                          self.conn._handle_xfrin_responses)
+        self.assertEqual(type(XfrinAXFREnd()), type(self.conn.get_xfrstate()))
+        self.assertEqual([], self.conn._datasrc_client.committed_diffs)
+
+    def test_axfr_response_qname_mismatch(self):
+        '''AXFR response with a mismatch question name.
+
+        Our implementation accepts that, so does BIND 9.
+
+        '''
+        self.axfr_response_params['question_1st'] = \
+            [Question(Name('mismatch.example'), TEST_RRCLASS, RRType.AXFR())]
+        self.conn.response_generator = self._create_normal_response_data
+        self.conn._send_query(RRType.AXFR())
+        self.conn._handle_xfrin_responses()
+        self.assertEqual(type(XfrinAXFREnd()), type(self.conn.get_xfrstate()))
+        check_diffs(self.assertEqual,
+                    [[('add', self._create_ns()), ('add', soa_rrset)]],
+                    self.conn._datasrc_client.committed_diffs)
+
+    def test_axfr_response_qclass_mismatch(self):
+        '''AXFR response with a mismatch RR class.
+
+        Our implementation accepts that, so does BIND 9.
+
+        '''
+        self.axfr_response_params['question_1st'] = \
+            [Question(TEST_ZONE_NAME, RRClass.CH(), RRType.AXFR())]
         self.conn.response_generator = self._create_normal_response_data
         self.conn.response_generator = self._create_normal_response_data
         self.conn._send_query(RRType.AXFR())
         self.conn._send_query(RRType.AXFR())
-        # two SOAs, and only these have been transfered.  the 2nd SOA is just
+        self.conn._handle_xfrin_responses()
-        # a marker, so only 1 RR has been provided in the iteration.
+        self.assertEqual(type(XfrinAXFREnd()), type(self.conn.get_xfrstate()))
-        self.assertEqual(self._handle_xfrin_response(), 1)
+        check_diffs(self.assertEqual,
+                    [[('add', self._create_ns()), ('add', soa_rrset)]],
+                    self.conn._datasrc_client.committed_diffs)
+
+    def test_axfr_response_qtype_mismatch(self):
+        '''AXFR response with a mismatch RR type.
+
+        Our implementation accepts that, so does BIND 9.
+
+        '''
+        # returning IXFR in question to AXFR query
+        self.axfr_response_params['question_1st'] = \
+            [Question(TEST_ZONE_NAME, RRClass.CH(), RRType.IXFR())]
+        self.conn.response_generator = self._create_normal_response_data
+        self.conn._send_query(RRType.AXFR())
+        self.conn._handle_xfrin_responses()
+        self.assertEqual(type(XfrinAXFREnd()), type(self.conn.get_xfrstate()))
+        check_diffs(self.assertEqual,
+                    [[('add', self._create_ns()), ('add', soa_rrset)]],
+                    self.conn._datasrc_client.committed_diffs)
+
+    def test_axfr_response_empty_question(self):
+        '''AXFR response with an empty question.
+
+        Our implementation accepts that, so does BIND 9.
+
+        '''
+        self.axfr_response_params['question_1st'] = []
+        self.conn.response_generator = self._create_normal_response_data
+        self.conn._send_query(RRType.AXFR())
+        self.conn._handle_xfrin_responses()
+        self.assertEqual(type(XfrinAXFREnd()), type(self.conn.get_xfrstate()))
+        check_diffs(self.assertEqual,
+                    [[('add', self._create_ns()), ('add', soa_rrset)]],
+                    self.conn._datasrc_client.committed_diffs)
 
 
     def test_do_xfrin(self):
     def test_do_xfrin(self):
         self.conn.response_generator = self._create_normal_response_data
         self.conn.response_generator = self._create_normal_response_data
@@ -991,9 +1130,10 @@ class TestAXFR(TestXfrinConnection):
             lambda key: self.__create_mock_tsig(key, TSIGError.NOERROR)
             lambda key: self.__create_mock_tsig(key, TSIGError.NOERROR)
         self.conn.response_generator = self._create_normal_response_data
         self.conn.response_generator = self._create_normal_response_data
         self.assertEqual(self.conn.do_xfrin(False), XFRIN_OK)
         self.assertEqual(self.conn.do_xfrin(False), XFRIN_OK)
-        # We use two messages in the tests.  The same context should have been
+        self.assertEqual(type(XfrinAXFREnd()), type(self.conn.get_xfrstate()))
-        # usef for both.
+        check_diffs(self.assertEqual,
-        self.assertEqual(2, self.conn._tsig_ctx.verify_called)
+                    [[('add', self._create_ns()), ('add', soa_rrset)]],
+                    self.conn._datasrc_client.committed_diffs)
 
 
     def test_do_xfrin_with_tsig_fail(self):
     def test_do_xfrin_with_tsig_fail(self):
         # TSIG verify will fail for the first message.  xfrin should fail
         # TSIG verify will fail for the first message.  xfrin should fail
@@ -1073,10 +1213,10 @@ class TestAXFR(TestXfrinConnection):
         self.conn.response_generator = self._create_broken_response_data
         self.conn.response_generator = self._create_broken_response_data
         self.assertEqual(self.conn.do_xfrin(False), XFRIN_FAIL)
         self.assertEqual(self.conn.do_xfrin(False), XFRIN_FAIL)
 
 
-    def test_do_xfrin_dberror(self):
+    def test_do_xfrin_datasrc_error(self):
-        # DB file is under a non existent directory, so its creation will fail,
+        # Emulate failure in the data source client on commit.
-        # which will make the transfer fail.
+        self.conn._datasrc_client.force_fail = True
-        self.conn._db_file = "not_existent/" + TEST_DB_FILE
+        self.conn.response_generator = self._create_normal_response_data
         self.assertEqual(self.conn.do_xfrin(False), XFRIN_FAIL)
         self.assertEqual(self.conn.do_xfrin(False), XFRIN_FAIL)
 
 
     def test_do_soacheck_and_xfrin(self):
     def test_do_soacheck_and_xfrin(self):
@@ -1331,8 +1471,8 @@ class TestIXFRSession(TestXfrinConnection):
         self._create_broken_response_data()
         self._create_broken_response_data()
         self.assertEqual(XFRIN_FAIL, self.conn.do_xfrin(False, RRType.IXFR()))
         self.assertEqual(XFRIN_FAIL, self.conn.do_xfrin(False, RRType.IXFR()))
 
 
-class TestIXFRSessionWithSQLite3(TestXfrinConnection):
+class TestXFRSessionWithSQLite3(TestXfrinConnection):
-    '''Tests for IXFR sessions using an SQLite3 DB.
+    '''Tests for XFR sessions using an SQLite3 DB.
 
 
     These are provided mainly to confirm the implementation actually works
     These are provided mainly to confirm the implementation actually works
     in an environment closer to actual operational environments.  So we
     in an environment closer to actual operational environments.  So we
@@ -1368,7 +1508,7 @@ class TestIXFRSessionWithSQLite3(TestXfrinConnection):
         result, soa = finder.find(name, type, None, ZoneFinder.FIND_DEFAULT)
         result, soa = finder.find(name, type, None, ZoneFinder.FIND_DEFAULT)
         return result == ZoneFinder.SUCCESS
         return result == ZoneFinder.SUCCESS
 
 
-    def test_do_xfrin_sqlite3(self):
+    def test_do_ixfrin_sqlite3(self):
         def create_ixfr_response():
         def create_ixfr_response():
             self.conn.reply_data = self.conn.create_response_data(
             self.conn.reply_data = self.conn.create_response_data(
                 questions=[Question(TEST_ZONE_NAME, TEST_RRCLASS,
                 questions=[Question(TEST_ZONE_NAME, TEST_RRCLASS,
@@ -1381,7 +1521,7 @@ class TestIXFRSessionWithSQLite3(TestXfrinConnection):
         self.assertEqual(XFRIN_OK, self.conn.do_xfrin(False, RRType.IXFR()))
         self.assertEqual(XFRIN_OK, self.conn.do_xfrin(False, RRType.IXFR()))
         self.assertEqual(1234, self.get_zone_serial())
         self.assertEqual(1234, self.get_zone_serial())
 
 
-    def test_do_xfrin_sqlite3_fail(self):
+    def test_do_ixfrin_sqlite3_fail(self):
         '''Similar to the previous test, but xfrin fails due to error.
         '''Similar to the previous test, but xfrin fails due to error.
 
 
         Check the DB is not changed.
         Check the DB is not changed.
@@ -1399,47 +1539,91 @@ class TestIXFRSessionWithSQLite3(TestXfrinConnection):
         self.assertEqual(XFRIN_FAIL, self.conn.do_xfrin(False, RRType.IXFR()))
         self.assertEqual(XFRIN_FAIL, self.conn.do_xfrin(False, RRType.IXFR()))
         self.assertEqual(1230, self.get_zone_serial())
         self.assertEqual(1230, self.get_zone_serial())
 
 
-    def test_do_xfrin_axfr_sqlite3(self):
+    def test_do_ixfrin_nozone_sqlite3(self):
-        '''AXFR-style IXFR.
+        self.conn._zone_name = Name('nosuchzone.example')
+        self.assertEqual(XFRIN_FAIL, self.conn.do_xfrin(False, RRType.IXFR()))
+        # This should fail even before starting state transition
+        self.assertEqual(None, self.conn.get_xfrstate())
+
+    def axfr_check(self, type):
+        '''Common checks for AXFR and AXFR-style IXFR
 
 
         '''
         '''
-        def create_ixfr_response():
+        def create_response():
             self.conn.reply_data = self.conn.create_response_data(
             self.conn.reply_data = self.conn.create_response_data(
-                questions=[Question(TEST_ZONE_NAME, TEST_RRCLASS,
+                questions=[Question(TEST_ZONE_NAME, TEST_RRCLASS, type)],
-                                    RRType.IXFR())],
                 answers=[soa_rrset, self._create_ns(), soa_rrset])
                 answers=[soa_rrset, self._create_ns(), soa_rrset])
-        self.conn.response_generator = create_ixfr_response
+        self.conn.response_generator = create_response
 
 
         # Confirm xfrin succeeds and SOA is updated, A RR is deleted.
         # Confirm xfrin succeeds and SOA is updated, A RR is deleted.
         self.assertEqual(1230, self.get_zone_serial())
         self.assertEqual(1230, self.get_zone_serial())
         self.assertTrue(self.record_exist(Name('dns01.example.com'),
         self.assertTrue(self.record_exist(Name('dns01.example.com'),
                                           RRType.A()))
                                           RRType.A()))
-        self.assertEqual(XFRIN_OK, self.conn.do_xfrin(False, RRType.IXFR()))
+        self.assertEqual(XFRIN_OK, self.conn.do_xfrin(False, type))
         self.assertEqual(1234, self.get_zone_serial())
         self.assertEqual(1234, self.get_zone_serial())
         self.assertFalse(self.record_exist(Name('dns01.example.com'),
         self.assertFalse(self.record_exist(Name('dns01.example.com'),
                                            RRType.A()))
                                            RRType.A()))
 
 
-    def test_do_xfrin_axfr_sqlite3_fail(self):
+    def test_do_ixfrin_axfr_sqlite3(self):
-        '''Similar to the previous test, but xfrin fails due to error.
+        '''AXFR-style IXFR.
+
+        '''
+        self.axfr_check(RRType.IXFR())
+
+    def test_do_axfrin_sqlite3(self):
+        '''AXFR.
+
+        '''
+        self.axfr_check(RRType.AXFR())
+
+    def axfr_failure_check(self, type):
+        '''Similar to the previous two tests, but xfrin fails due to error.
 
 
         Check the DB is not changed.
         Check the DB is not changed.
 
 
         '''
         '''
-        def create_ixfr_response():
+        def create_response():
             self.conn.reply_data = self.conn.create_response_data(
             self.conn.reply_data = self.conn.create_response_data(
-                questions=[Question(TEST_ZONE_NAME, TEST_RRCLASS,
+                questions=[Question(TEST_ZONE_NAME, TEST_RRCLASS, type)],
-                                    RRType.IXFR())],
                 answers=[soa_rrset, self._create_ns(), soa_rrset, soa_rrset])
                 answers=[soa_rrset, self._create_ns(), soa_rrset, soa_rrset])
-        self.conn.response_generator = create_ixfr_response
+        self.conn.response_generator = create_response
 
 
         self.assertEqual(1230, self.get_zone_serial())
         self.assertEqual(1230, self.get_zone_serial())
         self.assertTrue(self.record_exist(Name('dns01.example.com'),
         self.assertTrue(self.record_exist(Name('dns01.example.com'),
                                           RRType.A()))
                                           RRType.A()))
-        self.assertEqual(XFRIN_FAIL, self.conn.do_xfrin(False, RRType.IXFR()))
+        self.assertEqual(XFRIN_FAIL, self.conn.do_xfrin(False, type))
         self.assertEqual(1230, self.get_zone_serial())
         self.assertEqual(1230, self.get_zone_serial())
         self.assertTrue(self.record_exist(Name('dns01.example.com'),
         self.assertTrue(self.record_exist(Name('dns01.example.com'),
                                           RRType.A()))
                                           RRType.A()))
 
 
+    def test_do_xfrin_axfr_sqlite3_fail(self):
+        '''Failure case for AXFR-style IXFR.
+
+        '''
+        self.axfr_failure_check(RRType.IXFR())
+
+    def test_do_axfrin_sqlite3_fail(self):
+        '''Failure case for AXFR.
+
+        '''
+        self.axfr_failure_check(RRType.AXFR())
+
+    def test_do_axfrin_nozone_sqlite3(self):
+        def create_response():
+            # Within this test, owner names of the question/RRs don't matter,
+            # so we use pre-defined names (which are "out of zone") for
+            # simplicity.
+            self.conn.reply_data = self.conn.create_response_data(
+                questions=[Question(TEST_ZONE_NAME, TEST_RRCLASS,
+                                    RRType.AXFR())],
+                answers=[soa_rrset, self._create_ns(), soa_rrset, soa_rrset])
+        self.conn.response_generator = create_response
+        self.conn._zone_name = Name('nosuchzone.example')
+        self.assertEqual(XFRIN_FAIL, self.conn.do_xfrin(False, RRType.AXFR()))
+        # This should fail in the FirstData state
+        self.assertEqual(type(XfrinFirstData()),
+                         type(self.conn.get_xfrstate()))
+
 class TestXfrinRecorder(unittest.TestCase):
 class TestXfrinRecorder(unittest.TestCase):
     def setUp(self):
     def setUp(self):
         self.recorder = XfrinRecorder()
         self.recorder = XfrinRecorder()

+ 24 - 64
src/bin/xfrin/xfrin.py.in

@@ -148,8 +148,8 @@ class XfrinState:
     IXFR/AXFR response begins with an SOA).  When it reaches IXFREnd
     IXFR/AXFR response begins with an SOA).  When it reaches IXFREnd
     or AXFREnd, the process successfully completes.
     or AXFREnd, the process successfully completes.
 
 
-
+                             (AXFR or
-            (recv SOA)       (AXFR-style IXFR)   (SOA, add)
+            (recv SOA)        AXFR-style IXFR)  (SOA, add)
     InitialSOA------->FirstData------------->AXFR--------->AXFREnd
     InitialSOA------->FirstData------------->AXFR--------->AXFREnd
                           |                  |  ^         (post xfr
                           |                  |  ^         (post xfr
                           |                  |  |        checks, then
                           |                  |  |        checks, then
@@ -321,7 +321,7 @@ class XfrinFirstData(XfrinState):
         else:
         else:
             logger.debug(DBG_XFRIN_TRACE, XFRIN_GOT_NONINCREMENTAL_RESP,
             logger.debug(DBG_XFRIN_TRACE, XFRIN_GOT_NONINCREMENTAL_RESP,
                  conn.zone_str())
                  conn.zone_str())
-            # We are now goint to add RRs to the new zone.  We need create
+            # We are now going to add RRs to the new zone.  We need create
             # a Diff object.  It will be used throughtout the XFR session.
             # a Diff object.  It will be used throughtout the XFR session.
             conn._diff = Diff(conn._datasrc_client, conn._zone_name, True)
             conn._diff = Diff(conn._datasrc_client, conn._zone_name, True)
             self.set_xfrstate(conn, XfrinAXFR())
             self.set_xfrstate(conn, XfrinAXFR())
@@ -405,6 +405,12 @@ class XfrinAXFR(XfrinState):
         if rr.get_type() == RRType.SOA():
         if rr.get_type() == RRType.SOA():
             # SOA means end.  Don't commit it yet - we need to perform
             # SOA means end.  Don't commit it yet - we need to perform
             # post-transfer checks
             # post-transfer checks
+
+            soa_serial = get_soa_serial(rr.get_rdata()[0])
+            if conn._end_serial != soa_serial:
+                logger.warn(XFRIN_AXFR_INCONSISTENT_SOA, conn.zone_str(),
+                            conn._end_serial, soa_serial)
+
             self.set_xfrstate(conn, XfrinAXFREnd())
             self.set_xfrstate(conn, XfrinAXFREnd())
         # Yes, we've eaten this RR.
         # Yes, we've eaten this RR.
         return True
         return True
@@ -432,15 +438,14 @@ class XfrinConnection(asyncore.dispatcher):
     '''Do xfrin in this class. '''
     '''Do xfrin in this class. '''
 
 
     def __init__(self,
     def __init__(self,
-                 sock_map, zone_name, rrclass, datasrc_client, db_file,
+                 sock_map, zone_name, rrclass, datasrc_client,
-                 shutdown_event, master_addrinfo, tsig_key = None,
+                 shutdown_event, master_addrinfo, tsig_key=None,
-                 verbose=False, idle_timeout=60):
+                 idle_timeout=60):
-        '''Constructor of the XfrinConnection class.
+        '''Constructor of the XfirnConnection class.
 
 
         idle_timeout: max idle time for read data from socket.
         idle_timeout: max idle time for read data from socket.
         datasrc_client: the data source client object used for the XFR session.
         datasrc_client: the data source client object used for the XFR session.
                         This will eventually replace db_file completely.
                         This will eventually replace db_file completely.
-        db_file: specify the data source file (should soon be deprecated).
 
 
         '''
         '''
 
 
@@ -460,8 +465,7 @@ class XfrinConnection(asyncore.dispatcher):
         self._zone_name = zone_name
         self._zone_name = zone_name
         self._rrclass = rrclass
         self._rrclass = rrclass
 
 
-        # Data source handlers
+        # Data source handler
-        self._db_file = db_file # temporary for sqlite3 specific code
         self._datasrc_client = datasrc_client
         self._datasrc_client = datasrc_client
 
 
         self.create_socket(master_addrinfo[0], master_addrinfo[1])
         self.create_socket(master_addrinfo[0], master_addrinfo[1])
@@ -470,7 +474,6 @@ class XfrinConnection(asyncore.dispatcher):
         self._idle_timeout = idle_timeout
         self._idle_timeout = idle_timeout
         self.setblocking(1)
         self.setblocking(1)
         self._shutdown_event = shutdown_event
         self._shutdown_event = shutdown_event
-        self._verbose = verbose
         self._master_address = master_addrinfo[2]
         self._master_address = master_addrinfo[2]
         self._tsig_key = tsig_key
         self._tsig_key = tsig_key
         self._tsig_ctx = None
         self._tsig_ctx = None
@@ -648,16 +651,9 @@ class XfrinConnection(asyncore.dispatcher):
             if ret == XFRIN_OK:
             if ret == XFRIN_OK:
                 logger.info(XFRIN_XFR_TRANSFER_STARTED, request_str,
                 logger.info(XFRIN_XFR_TRANSFER_STARTED, request_str,
                             self.zone_str())
                             self.zone_str())
-                if self._request_type == RRType.IXFR():
+                self._send_query(self._request_type)
-                    self._request_type = RRType.IXFR()
+                self.__state = XfrinInitialSOA()
-                    self._send_query(self._request_type)
+                self._handle_xfrin_responses()
-                    self.__state = XfrinInitialSOA()
-                    self._handle_xfrin_responses()
-                else:
-                    self._send_query(self._request_type)
-                    isc.datasrc.sqlite3_ds.load(self._db_file,
-                                                self._zone_name.to_text(),
-                                                self._handle_axfrin_response)
                 logger.info(XFRIN_XFR_TRANSFER_SUCCESS, request_str,
                 logger.info(XFRIN_XFR_TRANSFER_SUCCESS, request_str,
                             self.zone_str())
                             self.zone_str())
 
 
@@ -665,11 +661,6 @@ class XfrinConnection(asyncore.dispatcher):
             logger.error(XFRIN_XFR_TRANSFER_FAILURE, request_str,
             logger.error(XFRIN_XFR_TRANSFER_FAILURE, request_str,
                          self.zone_str(), str(e))
                          self.zone_str(), str(e))
             ret = XFRIN_FAIL
             ret = XFRIN_FAIL
-        except isc.datasrc.sqlite3_ds.Sqlite3DSError as e:
-            # Note: this is old code and used only for AXFR.  This will be
-            # soon removed anyway, so we'll leave it.
-            logger.error(XFRIN_AXFR_DATABASE_FAILURE, self.zone_str(), str(e))
-            ret = XFRIN_FAIL
         except Exception as e:
         except Exception as e:
             # Catching all possible exceptions like this is generally not a
             # Catching all possible exceptions like this is generally not a
             # good practice, but handling an xfr session could result in
             # good practice, but handling an xfr session could result in
@@ -717,9 +708,6 @@ class XfrinConnection(asyncore.dispatcher):
 
 
         self._check_response_header(msg)
         self._check_response_header(msg)
 
 
-        if msg.get_rr_count(Message.SECTION_ANSWER) == 0:
-            raise XfrinException('answer section is empty')
-
         if msg.get_rr_count(Message.SECTION_QUESTION) > 1:
         if msg.get_rr_count(Message.SECTION_QUESTION) > 1:
             raise XfrinException('query section count greater than 1')
             raise XfrinException('query section count greater than 1')
 
 
@@ -775,31 +763,6 @@ class XfrinConnection(asyncore.dispatcher):
             if self._shutdown_event.is_set():
             if self._shutdown_event.is_set():
                 raise XfrinException('xfrin is forced to stop')
                 raise XfrinException('xfrin is forced to stop')
 
 
-    def _handle_axfrin_response(self):
-        '''Return a generator for the response to a zone transfer. '''
-        while True:
-            data_len = self._get_request_response(2)
-            msg_len = socket.htons(struct.unpack('H', data_len)[0])
-            recvdata = self._get_request_response(msg_len)
-            msg = Message(Message.PARSE)
-            msg.from_wire(recvdata)
-
-            # TSIG related checks, including an unexpected signed response
-            self._check_response_tsig(msg, recvdata)
-
-            # Perform response status validation
-            self._check_response_status(msg)
-
-            answer_section = msg.get_section(Message.SECTION_ANSWER)
-            for rr in self._handle_answer_section(answer_section):
-                yield rr
-
-            if self._soa_rr_count == 2:
-                break
-
-            if self._shutdown_event.is_set():
-                raise XfrinException('xfrin is forced to stop')
-
     def handle_read(self):
     def handle_read(self):
         '''Read query's response from socket. '''
         '''Read query's response from socket. '''
 
 
@@ -817,8 +780,8 @@ class XfrinConnection(asyncore.dispatcher):
         pass
         pass
 
 
 def process_xfrin(server, xfrin_recorder, zone_name, rrclass, db_file,
 def process_xfrin(server, xfrin_recorder, zone_name, rrclass, db_file,
-                  shutdown_event, master_addrinfo, check_soa, verbose,
+                  shutdown_event, master_addrinfo, check_soa, tsig_key,
-                  tsig_key, request_type):
+                  request_type):
     xfrin_recorder.increment(zone_name)
     xfrin_recorder.increment(zone_name)
 
 
     # Create a data source client used in this XFR session.  Right now we
     # Create a data source client used in this XFR session.  Right now we
@@ -834,8 +797,7 @@ def process_xfrin(server, xfrin_recorder, zone_name, rrclass, db_file,
     # Create a TCP connection for the XFR session and perform the operation.
     # Create a TCP connection for the XFR session and perform the operation.
     sock_map = {}
     sock_map = {}
     conn = XfrinConnection(sock_map, zone_name, rrclass, datasrc_client,
     conn = XfrinConnection(sock_map, zone_name, rrclass, datasrc_client,
-                           db_file, shutdown_event, master_addrinfo,
+                           shutdown_event, master_addrinfo, tsig_key)
-                           tsig_key, verbose)
     ret = XFRIN_FAIL
     ret = XFRIN_FAIL
     if conn.connect_to_master():
     if conn.connect_to_master():
         ret = conn.do_xfrin(check_soa, request_type)
         ret = conn.do_xfrin(check_soa, request_type)
@@ -977,13 +939,12 @@ class ZoneInfo:
                 (str(self.master_addr), self.master_port))
                 (str(self.master_addr), self.master_port))
 
 
 class Xfrin:
 class Xfrin:
-    def __init__(self, verbose = False):
+    def __init__(self):
         self._max_transfers_in = 10
         self._max_transfers_in = 10
         self._zones = {}
         self._zones = {}
         self._cc_setup()
         self._cc_setup()
         self.recorder = XfrinRecorder()
         self.recorder = XfrinRecorder()
         self._shutdown_event = threading.Event()
         self._shutdown_event = threading.Event()
-        self._verbose = verbose
 
 
     def _cc_setup(self):
     def _cc_setup(self):
         '''This method is used only as part of initialization, but is
         '''This method is used only as part of initialization, but is
@@ -1243,7 +1204,6 @@ class Xfrin:
                                                 db_file,
                                                 db_file,
                                                 self._shutdown_event,
                                                 self._shutdown_event,
                                                 master_addrinfo, check_soa,
                                                 master_addrinfo, check_soa,
-                                                self._verbose,
                                                 tsig_key, request_type))
                                                 tsig_key, request_type))
 
 
         xfrin_thread.start()
         xfrin_thread.start()
@@ -1263,9 +1223,9 @@ def set_signal_handler():
 
 
 def set_cmd_options(parser):
 def set_cmd_options(parser):
     parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
     parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
-            help="display more about what is going on")
+            help="This option is obsolete and has no effect.")
 
 
-def main(xfrin_class, use_signal = True):
+def main(xfrin_class, use_signal=True):
     """The main loop of the Xfrin daemon.
     """The main loop of the Xfrin daemon.
 
 
     @param xfrin_class: A class of the Xfrin object.  This is normally Xfrin,
     @param xfrin_class: A class of the Xfrin object.  This is normally Xfrin,
@@ -1282,7 +1242,7 @@ def main(xfrin_class, use_signal = True):
 
 
         if use_signal:
         if use_signal:
             set_signal_handler()
             set_signal_handler()
-        xfrind = xfrin_class(verbose = options.verbose)
+        xfrind = xfrin_class()
         xfrind.startup()
         xfrind.startup()
     except KeyboardInterrupt:
     except KeyboardInterrupt:
         logger.info(XFRIN_STOPPED_BY_KEYBOARD)
         logger.info(XFRIN_STOPPED_BY_KEYBOARD)

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

@@ -103,3 +103,19 @@ which is the RR following the initial SOA.  Non incremental transfer is
 either AXFR or AXFR-style IXFR.  In the latter case, it means that
 either AXFR or AXFR-style IXFR.  In the latter case, it means that
 in a response to IXFR query the first data is not SOA or its SOA serial
 in a response to IXFR query the first data is not SOA or its SOA serial
 is not equal to the requested SOA serial.
 is not equal to the requested SOA serial.
+
+% XFRIN_AXFR_INCONSISTENT_SOA AXFR SOAs are inconsistent for %1: %2 expected, %3 received
+The serial fields of the first and last SOAs of AXFR (including AXFR-style
+IXFR) are not the same.  According to RFC 5936 these two SOAs must be the
+"same" (not only for the serial), but it is still not clear what the
+receiver should do if this condition does not hold.  There was a discussion
+about this at the IETF dnsext wg:
+http://www.ietf.org/mail-archive/web/dnsext/current/msg07908.html
+and the general feeling seems that it would be better to reject the
+transfer if a mismatch is detected.  On the other hand, also as noted
+in that email thread, neither BIND 9 nor NSD performs any comparison
+on the SOAs.  For now, we only check the serials (ignoring other fields)
+and only leave a warning log message when a mismatch is found.  If it
+turns out to happen with a real world primary server implementation
+and that server actually feeds broken data (e.g. mixed versions of
+zone), we can consider a stricter action.

+ 1 - 1
src/lib/python/isc/datasrc/client_inc.cc

@@ -110,7 +110,7 @@ Return an updater to make updates to a specific zone.\n\
 The RR class of the zone is the one that the client is expected to\n\
 The RR class of the zone is the one that the client is expected to\n\
 handle (see the detailed description of this class).\n\
 handle (see the detailed description of this class).\n\
 \n\
 \n\
-If the specified zone is not found via the client, a NULL pointer will\n\
+If the specified zone is not found via the client, a None object will\n\
 be returned; in other words a completely new zone cannot be created\n\
 be returned; in other words a completely new zone cannot be created\n\
 using an updater. It must be created beforehand (even if it's an empty\n\
 using an updater. It must be created beforehand (even if it's an empty\n\
 placeholder) in a way specific to the underlying data source.\n\
 placeholder) in a way specific to the underlying data source.\n\

+ 6 - 3
src/lib/python/isc/datasrc/client_python.cc

@@ -120,9 +120,12 @@ DataSourceClient_getUpdater(PyObject* po_self, PyObject* args) {
         PyBool_Check(replace_obj)) {
         PyBool_Check(replace_obj)) {
         bool replace = (replace_obj != Py_False);
         bool replace = (replace_obj != Py_False);
         try {
         try {
-            return (createZoneUpdaterObject(
+            ZoneUpdaterPtr updater =
-                        self->cppobj->getUpdater(PyName_ToName(name_obj),
+                self->cppobj->getUpdater(PyName_ToName(name_obj), replace);
-                                                 replace)));
+            if (!updater) {
+                return (Py_None);
+            }
+            return (createZoneUpdaterObject(updater));
         } catch (const isc::NotImplemented& ne) {
         } catch (const isc::NotImplemented& ne) {
             PyErr_SetString(getDataSourceException("NotImplemented"),
             PyErr_SetString(getDataSourceException("NotImplemented"),
                             ne.what());
                             ne.what());

+ 5 - 0
src/lib/python/isc/datasrc/tests/datasrc_test.py

@@ -383,6 +383,11 @@ class DataSrcUpdater(unittest.TestCase):
         self.assertEqual("www.example.com. 3600 IN A 192.0.2.1\n",
         self.assertEqual("www.example.com. 3600 IN A 192.0.2.1\n",
                          rrset.to_text())
                          rrset.to_text())
 
 
+    def test_update_for_no_zone(self):
+        dsc = isc.datasrc.DataSourceClient(WRITE_ZONE_DB_FILE)
+        self.assertEqual(None,
+                         dsc.get_updater(isc.dns.Name("notexistent.example"),
+                                         True))
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
     isc.log.init("bind10")
     isc.log.init("bind10")