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]
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()
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())
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)
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.
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")
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)
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
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)
# 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)
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
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):