Commit 73721a97 authored by JINMEI Tatuya's avatar JINMEI Tatuya
Browse files

[1299] pre-work refactoring: get the zone SOA in the XfrinConnection

as it will be used for other purposes than IXFR.  If it's not found,
just log it and set it to None instead of raising an exception; what
should happen depends on how it's used, so it now lets the later process
decide what to do in that case.
parent 6764e7da
......@@ -224,7 +224,7 @@ class MockXfrinConnection(XfrinConnection):
def __init__(self, sock_map, zone_name, rrclass, datasrc_client,
shutdown_event, master_addr, tsig_key=None):
super().__init__(sock_map, zone_name, rrclass, MockDataSourceClient(),
shutdown_event, master_addr)
shutdown_event, master_addr, TEST_DB_FILE)
self.query_data = b''
self.reply_data = b''
self.force_time_out = False
......@@ -688,6 +688,15 @@ class TestXfrinConnection(unittest.TestCase):
rrset.add_rdata(Rdata(RRType.NS(), TEST_RRCLASS, nsname))
return rrset
def _set_test_zone(self, zone_name):
'''Set the zone name for transfer to the specified one.
It also make sure that the SOA RR (if exist) is correctly (re)set.
'''
self.conn._zone_name = zone_name
self.conn._zone_soa = self.conn._get_zone_soa()
class TestAXFR(TestXfrinConnection):
def setUp(self):
super().setUp()
......@@ -788,19 +797,20 @@ class TestAXFR(TestXfrinConnection):
# 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._set_test_zone(Name('no-such-zone.example'))
self.assertRaises(XfrinException, self.conn._create_query,
RRType.IXFR())
self.conn._zone_name = Name('partial-match-zone.example')
self._set_test_zone(Name('partial-match-zone.example'))
self.assertRaises(XfrinException, self.conn._create_query,
RRType.IXFR())
self.conn._zone_name = Name('no-soa.example')
self._set_test_zone(Name('no-soa.example'))
self.assertRaises(XfrinException, self.conn._create_query,
RRType.IXFR())
self.conn._zone_name = Name('dup-soa.example')
self._set_test_zone(Name('dup-soa.example'))
self.conn._zone_soa = self.conn._get_zone_soa()
self.assertRaises(XfrinException, self.conn._create_query,
RRType.IXFR())
......@@ -1273,7 +1283,6 @@ class TestIXFRResponse(TestXfrinConnection):
self.conn._query_id = self.conn.qid = 1035
self.conn._request_serial = isc.dns.Serial(1230)
self.conn._request_type = RRType.IXFR()
self._zone_name = TEST_ZONE_NAME
self.conn._datasrc_client = MockDataSourceClient()
XfrinInitialSOA().set_xfrstate(self.conn, XfrinInitialSOA())
......@@ -1580,7 +1589,7 @@ class TestXFRSessionWithSQLite3(TestXfrinConnection):
self.assertEqual(1230, self.get_zone_serial().get_value())
def test_do_ixfrin_nozone_sqlite3(self):
self.conn._zone_name = Name('nosuchzone.example')
self._set_test_zone(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())
......@@ -1666,7 +1675,7 @@ class TestXFRSessionWithSQLite3(TestXfrinConnection):
RRType.AXFR())],
answers=[soa_rrset, self._create_ns(), soa_rrset])
self.conn.response_generator = create_response
self.conn._zone_name = Name('example.com')
self._set_test_zone(Name('example.com'))
self.assertEqual(XFRIN_OK, self.conn.do_xfrin(False, RRType.AXFR()))
self.assertEqual(type(XfrinAXFREnd()),
type(self.conn.get_xfrstate()))
......
......@@ -473,10 +473,13 @@ class XfrinConnection(asyncore.dispatcher):
def __init__(self,
sock_map, zone_name, rrclass, datasrc_client,
shutdown_event, master_addrinfo, tsig_key=None,
shutdown_event, master_addrinfo, db_file, tsig_key=None,
idle_timeout=60):
'''Constructor of the XfirnConnection class.
db_file: SQLite3 DB file. Unforutnately we still need this for
temporary workaround in _get_zone_soa(). This should be
removed when we eliminate the need for the workaround.
idle_timeout: max idle time for read data from socket.
datasrc_client: the data source client object used for the XFR session.
This will eventually replace db_file completely.
......@@ -500,7 +503,9 @@ class XfrinConnection(asyncore.dispatcher):
self._rrclass = rrclass
# Data source handler
self._db_file = db_file
self._datasrc_client = datasrc_client
self._zone_soa = self._get_zone_soa()
self._sock_map = sock_map
self._soa_rr_count = 0
......@@ -524,6 +529,55 @@ class XfrinConnection(asyncore.dispatcher):
self.create_socket(self._master_addrinfo[0], self._master_addrinfo[1])
self.setblocking(1)
def _get_zone_soa(self):
'''Retrieve the current SOA RR of the zone to be transferred.
It will be used for various purposes in subsequent xfr protocol
processing. It is validly possible that the zone is currently
empty and therefore doesn't have an SOA, so this method doesn't
consider it an error and returns None in such a case. It may or
may not result in failure in the actual processing depending on
how the SOA is used.
When the zone has an SOA RR, this method makes sure that it's
valid, i.e., it has exactly one RDATA; if it is not the case
this method returns None.
If the underlying data source doesn't even know the zone, this method
tries to provide backward compatible behavior where xfrin is
responsible for creating zone in the corresponding DB table.
For a longer term we should deprecate this behavior by introducing
more generic zone management framework, but at the moment we try
to not surprise existing users. (Note also that the part of
providing the compatible behavior uses the old data source API.
We'll deprecate this API in a near future, too).
'''
# 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:
# The data source doesn't know the zone. For now, we provide
# backward compatibility and creates a new one ourselves.
isc.datasrc.sqlite3_ds.load(self._db_file,
self._zone_name.to_text(),
lambda : [])
logger.warn(XFRIN_ZONE_CREATED, self.zone_str())
# try again
result, finder = self._datasrc_client.find_zone(self._zone_name)
if result != DataSourceClient.SUCCESS:
return None
result, soa_rrset = finder.find(self._zone_name, RRType.SOA(),
None, ZoneFinder.FIND_DEFAULT)
if result != ZoneFinder.SUCCESS:
logger.info(XFRIN_ZONE_NO_SOA, self.zone_str())
return None
if soa_rrset.get_rdata_count() != 1:
logger.warn(XFRIN_ZONE_MULTIPLE_SOA, self.zone_str(),
soa_rrset.get_rdata_count())
return None
return soa_rrset
def __set_xfrstate(self, new_state):
self.__state = new_state
......@@ -545,37 +599,16 @@ class XfrinConnection(asyncore.dispatcher):
str(e))
return False
def _get_zone_soa(self):
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, a working zone 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()))
return soa_rrset
def _create_query(self, query_type):
'''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.
query_type is either SOA, AXFR or IXFR. An IXFR query needs the
zone's current SOA record. If it's not known, 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)
......@@ -586,26 +619,13 @@ class XfrinConnection(asyncore.dispatcher):
msg.set_rcode(Rcode.NOERROR())
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.
zone_soa_rr = self._get_zone_soa()
msg.add_rrset(Message.SECTION_AUTHORITY, zone_soa_rr)
self._request_serial = get_soa_serial(zone_soa_rr.get_rdata()[0])
else:
# For AXFR, we temporarily provide backward compatible behavior
# where xfrin is responsible for creating zone in the corresponding
# DB table. Note that the code below uses the old data source
# API and assumes SQLite3 in an ugly manner. We'll have to
# develop a better way of managing zones in a generic way and
# eliminate the code like the one here.
try:
self._get_zone_soa()
except XfrinException:
def empty_rr_generator():
return []
isc.datasrc.sqlite3_ds.load(self._db_file,
self._zone_name.to_text(),
empty_rr_generator)
if self._zone_soa is None:
# (incremental) IXFR doesn't work without known SOA
raise XfrinException('Failed to create IXFR query due to no ' +
'SOA for ' + self.zone_str())
msg.add_rrset(Message.SECTION_AUTHORITY, self._zone_soa)
self._request_serial = \
get_soa_serial(self._zone_soa.get_rdata()[0])
return msg
def _send_data(self, data):
......@@ -840,11 +860,9 @@ def __process_xfrin(server, zone_name, rrclass, db_file,
while retry:
retry = False
conn = conn_class(sock_map, zone_name, rrclass, datasrc_client,
shutdown_event, master_addrinfo, tsig_key)
shutdown_event, master_addrinfo, db_file,
tsig_key)
conn.init_socket()
# XXX: We still need _db_file for temporary workaround in _create_query().
# This should be removed when we eliminate the need for the workaround.
conn._db_file = db_file
ret = XFRIN_FAIL
if conn.connect_to_master():
ret = conn.do_xfrin(check_soa, request_type)
......
......@@ -15,16 +15,41 @@
# No namespace declaration - these constants go in the global namespace
# of the xfrin messages python module.
% XFRIN_ZONE_CREATED Zone %1 not found in the given data source, newly crated
On starting an xfrin session, it is identified that the zone to be
transferred is not found in the data source. This can happen if a
secondary DNS server first tries to perform AXFR from a primary server
without creating the zone image beforehand (e.g. by b10-loadzone). As
of this writing the xfrin process provides backward compatible
behavior to previous versions: creating a new one in the data source
not to surprise existing users too much. This is probably not a good
idea, however, in terms of who should be responsible for managing
zones at a higher level. In future it is more likely that a separate
zone management framework is provided, and the situation where the
given zone isn't found in xfrout will be treated as an error.
% XFRIN_ZONE_NO_SOA Zone %1 does not have SOA
On starting an xfrin session, it is identified that the zone to be
transferred does not have an SOA RR in the data source. This is not
necessarily an error; if a secondary DNS server first tries to perform
transfer from a primary server, the zone can be empty, and therefore
doesn't have an SOA. Subsequent AXFR will fill in the zone; if the
attempt is IXFR it will fail in query creation.
% XFRIN_ZONE_MULTIPLE_SOA Zone %1 has %2 SOA RRs
On starting an xfrin session, it is identified that the zone to be
transferred has multiple SOA RRs. Such a zone is broken, but could be
accidentally configured especially in a data source using "non
captive" backend database. The implementation ignores entire SOA RRs
and tries to continue processing as if the zone were empty. This
means subsequent AXFR can succeed and possibly replace the zone with
valid content, but an IXFR attempt will fail.
% XFRIN_XFR_OTHER_FAILURE %1 transfer of zone %2 failed: %3
The XFR transfer for the given zone has failed due to a problem outside
of the xfrin module. Possible reasons are a broken DNS message or failure
in database connection. The error is shown in the log message.
% XFRIN_AXFR_DATABASE_FAILURE AXFR transfer of zone %1 failed: %2
The AXFR transfer for the given zone has failed due to a database problem.
The error is shown in the log message. Note: due to the code structure
this can only happen for AXFR.
% XFRIN_XFR_TRANSFER_FAILURE %1 transfer of zone %2 failed: %3
The XFR transfer for the given zone has failed due to a protocol error.
The error is shown in the log message.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment