Browse Source

[1261] extended _create_query() so that it can send an IXFR query.

JINMEI Tatuya 13 years ago
parent
commit
e8e1bd309d
2 changed files with 140 additions and 13 deletions
  1. 88 3
      src/bin/xfrin/tests/xfrin_test.py
  2. 52 10
      src/bin/xfrin/xfrin.py.in

+ 88 - 3
src/bin/xfrin/tests/xfrin_test.py

@@ -43,11 +43,20 @@ TEST_MASTER_PORT = '53535'
 
 TSIG_KEY = TSIGKey("example.com:SFuWd/q99SzF8Yzd1QbB9g==")
 
+# SOA intended to be used for the new SOA as a result of transfer.
 soa_rdata = Rdata(RRType.SOA(), TEST_RRCLASS,
                   'master.example.com. admin.example.com ' +
                   '1234 3600 1800 2419200 7200')
 soa_rrset = RRset(TEST_ZONE_NAME, TEST_RRCLASS, RRType.SOA(), RRTTL(3600))
 soa_rrset.add_rdata(soa_rdata)
+
+# SOA intended to be used for the current SOA at the secondary side.
+# Note that its serial is smaller than that of soa_rdata.
+begin_soa_rdata = Rdata(RRType.SOA(), TEST_RRCLASS,
+                        'master.example.com. admin.example.com ' +
+                        '1230 3600 1800 2419200 7200')
+begin_soa_rrset = RRset(TEST_ZONE_NAME, TEST_RRCLASS, RRType.SOA(), RRTTL(3600))
+begin_soa_rrset.add_rdata(begin_soa_rdata)
 example_axfr_question = Question(TEST_ZONE_NAME, TEST_RRCLASS, RRType.AXFR())
 example_soa_question = Question(TEST_ZONE_NAME, TEST_RRCLASS, RRType.SOA())
 default_questions = [example_axfr_question]
@@ -107,6 +116,48 @@ class MockXfrinConnection(XfrinConnection):
         self.qid = None
         self.response_generator = None
 
+    # The following three implement a simplified mock of DataSourceClient
+    # and ZoneFinder classes for testing purposes.
+    def _get_datasrc_client(self, rrclass):
+        return self
+
+    def find_zone(self, zone_name):
+        '''Mock DataSourceClient.find_zone().
+
+        It returns itself (subsequently acting as a mock ZoneFinder) for
+        some test zone names.  For some others it returns either NOTFOUND
+        or PARTIALMATCH.
+
+        '''
+        if zone_name == TEST_ZONE_NAME or \
+                zone_name == Name('no-soa.example') or \
+                zone_name == Name('dup-soa.example'):
+            return (isc.datasrc.DataSourceClient.SUCCESS, self)
+        elif zone_name == Name('no-such-zone.example'):
+            return (DataSourceClient.NOTFOUND, None)
+        elif zone_name == Name('partial-match-zone.example'):
+            return (DataSourceClient.PARTIALMATCH, self)
+        raise ValueError('Unexpected input to mock client: bug in test case?')
+
+    def find(self, name, rrtype, target, options):
+        '''Mock ZoneFinder.find().
+
+        It returns the predefined SOA RRset to queries for SOA of the common
+        test zone name.  It also emulates some unusual cases for special
+        zone names.
+
+        '''
+        if name == TEST_ZONE_NAME and rrtype == RRType.SOA():
+            return (ZoneFinder.SUCCESS, begin_soa_rrset)
+        if name == Name('no-soa.example'):
+            return (ZoneFinder.NXDOMAIN, None)
+        if name == Name('dup-soa.example'):
+            dup_soa_rrset = RRset(name, TEST_RRCLASS, RRType.SOA(), RRTTL(0))
+            dup_soa_rrset.add_rdata(begin_soa_rdata)
+            dup_soa_rrset.add_rdata(soa_rdata)
+            return (ZoneFinder.SUCCESS, dup_soa_rrset)
+        raise ValueError('Unexpected input to mock finder: bug in test case?')
+
     def _asyncore_loop(self):
         if self.force_close:
             self.handle_close()
@@ -450,7 +501,7 @@ class TestXfrinConnection(unittest.TestCase):
         c.close()
 
     def test_create_query(self):
-        def check_query(expected_qtype, expected_authority):
+        def check_query(expected_qtype, expected_auth):
             '''Helper method to repeat the same pattern of tests'''
             self.assertEqual(Opcode.QUERY(), msg.get_opcode())
             self.assertEqual(Rcode.NOERROR(), msg.get_rcode())
@@ -459,15 +510,24 @@ class TestXfrinConnection(unittest.TestCase):
             self.assertEqual(expected_qtype, msg.get_question()[0].get_type())
             self.assertEqual(0, msg.get_rr_count(Message.SECTION_ANSWER))
             self.assertEqual(0, msg.get_rr_count(Message.SECTION_ADDITIONAL))
-            if expected_authority is None:
+            if expected_auth is None:
                 self.assertEqual(0,
                                  msg.get_rr_count(Message.SECTION_AUTHORITY))
             else:
                 self.assertEqual(1,
                                  msg.get_rr_count(Message.SECTION_AUTHORITY))
+                auth_rr = msg.get_section(Message.SECTION_AUTHORITY)[0]
+                self.assertEqual(expected_auth.get_name(), auth_rr.get_name())
+                self.assertEqual(expected_auth.get_type(), auth_rr.get_type())
+                self.assertEqual(expected_auth.get_class(),
+                                 auth_rr.get_class())
+                # In our test scenario RDATA must be 1
+                self.assertEqual(1, expected_auth.get_rdata_count())
+                self.assertEqual(1, auth_rr.get_rdata_count())
+                self.assertEqual(expected_auth.get_rdata()[0],
+                                 auth_rr.get_rdata()[0])
 
         # Actual tests start here
-
         # SOA query
         msg = self.conn._create_query(RRType.SOA())
         check_query(RRType.SOA(), None)
@@ -476,6 +536,31 @@ class TestXfrinConnection(unittest.TestCase):
         msg = self.conn._create_query(RRType.AXFR())
         check_query(RRType.AXFR(), None)
 
+        # IXFR query
+        msg = self.conn._create_query(RRType.IXFR())
+        check_query(RRType.IXFR(), begin_soa_rrset)
+        self.assertEqual(1230, self.conn._request_serial)
+
+    def test_create_ixfr_query_fail(self):
+        # In these cases _create_query() will fail to find a valid SOA RR to
+        # insert in the IXFR query, and should raise an exception.
+
+        self.conn._zone_name = Name('no-such-zone.example')
+        self.assertRaises(XfrinException, self.conn._create_query,
+                          RRType.IXFR())
+
+        self.conn._zone_name = Name('partial-match-zone.example')
+        self.assertRaises(XfrinException, self.conn._create_query,
+                          RRType.IXFR())
+
+        self.conn._zone_name = Name('no-soa.example')
+        self.assertRaises(XfrinException, self.conn._create_query,
+                          RRType.IXFR())
+
+        self.conn._zone_name = Name('dup-soa.example')
+        self.assertRaises(XfrinException, self.conn._create_query,
+                          RRType.IXFR())
+
     def test_send_query(self):
         def message_has_tsig(data):
             # a simple check if the actual data contains a TSIG RR.

+ 52 - 10
src/bin/xfrin/xfrin.py.in

@@ -28,8 +28,9 @@ from optparse import OptionParser, OptionValueError
 from isc.config.ccsession import *
 from isc.notify import notify_out
 import isc.util.process
+from isc.datasrc import DataSourceClient, ZoneFinder
 import isc.net.parse
-import isc.xfrin.diff
+from isc.xfrin.diff import Diff
 from isc.log_messages.xfrin_messages import *
 
 isc.log.init("b10-xfrin")
@@ -181,7 +182,7 @@ class XfrinIXFRDeleteSOA(XfrinState):
     def handle_rr(self, conn, rr):
         if rr.get_type() != RRType.SOA():
             # this shouldn't happen; should this occur it means an internal
-            #
+            # bug.
             raise XfrinException(rr.get_type().to_text() + \
                                      ' RR is given in IXFRDeleteSOA state')
         conn._diff.remove_data(rr)
@@ -191,6 +192,7 @@ class XfrinIXFRDeleteSOA(XfrinState):
 class XfrinIXFRDelete(XfrinState):
     def handle_rr(self, conn, rr):
         if rr.get_type() == RRType.SOA():
+            # This is the only place where current_serial is set
             conn._current_serial = get_soa_serial(rr.get_rdata()[0])
             self.set_xfrstate(conn, XfrinIXFRAddSOA())
             return False
@@ -201,7 +203,7 @@ class XfrinIXFRAddSOA(XfrinState):
     def handle_rr(self, conn, rr):
         if rr.get_type() != RRType.SOA():
             # this shouldn't happen; should this occur it means an internal
-            #
+            # bug.
             raise XfrinException(rr.get_type().to_text() + \
                                      ' RR is given in IXFRAddSOA state')
         conn._diff.add_data(rr)
@@ -256,12 +258,17 @@ class XfrinConnection(asyncore.dispatcher):
         # transfer type may differ due to IXFR->AXFR fallback:
         self._request_type = None
         self._end_serial = None # essentially private
-        self._current_serial = None
-        self.create_socket(master_addrinfo[0], master_addrinfo[1])
+
+        # Zone parameters
         self._zone_name = zone_name
-        self._sock_map = sock_map
         self._rrclass = rrclass
-        self._db_file = db_file
+
+        # Data source handlers
+        self._db_file = db_file # temporary for sqlite3 specific code
+        self._datasrc_client = self._get_datasrc_client(rrclass)
+
+        self.create_socket(master_addrinfo[0], master_addrinfo[1])
+        self._sock_map = sock_map
         self._soa_rr_count = 0
         self._idle_timeout = idle_timeout
         self.setblocking(1)
@@ -277,6 +284,10 @@ class XfrinConnection(asyncore.dispatcher):
     def __create_tsig_ctx(self, key):
         return TSIGContext(key)
 
+    def _get_datasrc_client(self, rrclass):
+        # Create a client here once #1206 is done
+        return None
+
     def __set_xfrstate(self, new_state):
         self.__state = new_state
 
@@ -298,16 +309,47 @@ class XfrinConnection(asyncore.dispatcher):
             return False
 
     def _create_query(self, query_type):
-        '''Create dns query message. '''
+        '''Create an XFR-related query message.
+
+        query_type is either SOA, AXFR or IXFR.  For type IXFR, it searches
+        the associated data source for the current SOA record to include
+        it in the query.  If the corresponding zone or the SOA record
+        cannot be found, it raises an XfrinException exception.  Note that
+        this may not necessarily a broken configuration; for the first attempt
+        of transfer the secondary may not have any boot-strap zone
+        information, in which case IXFR simply won't work.  The xfrin
+        should then fall back to AXFR.  _request_serial is recorded for
+        later use.
 
+        '''
         msg = Message(Message.RENDER)
         query_id = random.randint(0, 0xFFFF)
         self._query_id = query_id
         msg.set_qid(query_id)
         msg.set_opcode(Opcode.QUERY())
         msg.set_rcode(Rcode.NOERROR())
-        query_question = Question(self._zone_name, self._rrclass, query_type)
-        msg.add_question(query_question)
+        msg.add_question(Question(self._zone_name, self._rrclass, query_type))
+        if query_type == RRType.IXFR():
+            # get the zone finder.  this must be SUCCESS (not even
+            # PARTIALMATCH) because we are specifying the zone origin name.
+            result, finder = self._datasrc_client.find_zone(self._zone_name)
+            if result != DataSourceClient.SUCCESS:
+                raise XfrinException('Zone not found in the given data ' +
+                                     'source: ' + self.zone_str())
+            result, soa_rrset = finder.find(self._zone_name, RRType.SOA(),
+                                            None, ZoneFinder.FIND_DEFAULT)
+            if result != ZoneFinder.SUCCESS:
+                raise XfrinException('SOA RR not found in zone: ' +
+                                     self.zone_str())
+            # Especially for database-based zones, working zones may be in
+            # a broken state where it has more than one SOA RR.  We proactively
+            # check the condition and abort the xfr attempt if we identify it.
+            if soa_rrset.get_rdata_count() != 1:
+                raise XfrinException('Invalid number of SOA RRs for ' +
+                                     self.zone_str() + ': ' +
+                                     str(soa_rrset.get_rdata_count()))
+            msg.add_rrset(Message.SECTION_AUTHORITY, soa_rrset)
+            self._request_serial = get_soa_serial(soa_rrset.get_rdata()[0])
         return msg
 
     def _send_data(self, data):