Browse Source

[master] Added ChangeLog entry 623 for trac 2955.

Thomas Markwalder 12 years ago
parent
commit
f62ca4a0b6

+ 12 - 1
ChangeLog

@@ -1,10 +1,21 @@
-622.	[func]		tmark
+623.	[func]		tmark
 	Created the initial, bare-bones implementation of DHCP-DDNS service
 	Created the initial, bare-bones implementation of DHCP-DDNS service
 	process class, D2Process, and the abstract class from which it derives,
 	process class, D2Process, and the abstract class from which it derives,
 	DProcessBase. D2Process will provide the DHCP-DDNS specific event loop
 	DProcessBase. D2Process will provide the DHCP-DDNS specific event loop
 	and business logic.
 	and business logic.
 	(Trac #2955, git dbe4772246039a1257b6492936fda2a8600cd245)
 	(Trac #2955, git dbe4772246039a1257b6492936fda2a8600cd245)
 
 
+622.	[func]*		jinmei
+	b10-xfrin now has tighter control on the choice of IXFR or AXFR
+	through zones/request_ixfr configuration item.  It includes
+	the new "IXFR only" behavior for some special cases.  b10-xfrin
+	now also uses AXFR whenever necessary, so it is now safe to try
+	IXFR by default and it's made the default.  The previous
+	use_ixfr configuration item was deprecated and triggers startup
+	failure if specified; configuration using use_ixfr should be
+	updated.
+	(Trac #2911, git 8118f8e4e9c0ad3e7b690bbce265a163e4f8767a)
+
 621.	[func]		team
 621.	[func]		team
 	libdns++: All Rdata classes now use the generic lexer in
 	libdns++: All Rdata classes now use the generic lexer in
 	constructors from text. This means that the name fields in such
 	constructors from text. This means that the name fields in such

+ 93 - 32
doc/guide/bind10-guide.xml

@@ -2725,11 +2725,8 @@ TODO
 
 
     <para>
     <para>
       The <command>b10-xfrin</command> process supports both AXFR and
       The <command>b10-xfrin</command> process supports both AXFR and
-      IXFR.  Due to some implementation limitations of the current
-      development release, however, it only tries AXFR by default,
-      and care should be taken to enable IXFR.
+      IXFR.
     </para>
     </para>
-<!-- TODO: http://bind10.isc.org/ticket/1279 -->
 
 
     <section>
     <section>
       <title>Configuration for Incoming Zone Transfers</title>
       <title>Configuration for Incoming Zone Transfers</title>
@@ -2763,34 +2760,82 @@ TODO
 &gt; <userinput>config set Xfrin/zones[0]/tsig_key "<option>example.key</option>"</userinput>
 &gt; <userinput>config set Xfrin/zones[0]/tsig_key "<option>example.key</option>"</userinput>
     </section>
     </section>
 
 
-    <section>
-      <title>Enabling IXFR</title>
-      <para>
-        As noted above, <command>b10-xfrin</command> uses AXFR for
-        zone transfers by default.  To enable IXFR for zone transfers
-        for a particular zone, set the <varname>use_ixfr</varname>
-        configuration parameter to <quote>true</quote>.
-        In the above example of configuration sequence, you'll need
-        to add the following before performing <userinput>commit</userinput>:
-      <screen>&gt; <userinput>config set Xfrin/zones[0]/use_ixfr true</userinput></screen>
-      </para>
+    <section id="request_ixfr">
+      <title>Control the use of IXFR</title>
+      <para>
+	By default, <command>b10-xfrin</command> uses IXFR for
+	transferring zones specified in
+	the <varname>Xfrin/zones</varname> list of the configuration,
+	unless it doesn't know the current SOA serial of the zone
+	(including the case where the zone has never transferred or
+	locally loaded), in which case it automatically uses AXFR.
+	If the attempt of IXFR fails, <command>b10-xfrin</command>
+	automatically retries the transfer using AXFR.
+	In general, this works for any master server implementations
+	including those that don't support IXFR and in any local state
+	of the zone.  So there should normally be no need to configure
+	on whether to use IXFR.
+      </para>
+
+      <para>
+	In some cases, however, it may be desirable to specify how and
+	whether to use IXFR and AXFR.
+	The <varname>request_ixfr</varname> configuration item under
+	<varname>Xfrin/zones</varname> can be used to control such
+	policies.
+	It can take the following values.
+      </para>
+      <variablelist>
+	<varlistentry>
+	  <term>yes</term>
+	  <listitem>
+	    <simpara>
+	      This is the default behavior as described above.
+	    </simpara>
+	  </listitem>
+	</varlistentry>
+	<varlistentry>
+	  <term>no</term>
+	  <listitem>
+	    <simpara>
+	      Only use AXFR.  Note that this value normally shouldn't
+	      be needed thanks to the automatic fallback from IXFR to IXFR.
+	      A possible case where this value needs to be used is
+	      that the master server has a bug and crashes if it
+	      receives an IXFR request.
+	    </simpara>
+	  </listitem>
+	</varlistentry>
+	<varlistentry>
+	  <term>only</term>
+	  <listitem>
+	    <simpara>
+	      Only use IXFR except when the current SOA serial is not
+	      known.
+	      This value has a severe drawback, that is, if the master
+	      server does not support IXFR zone transfers never
+	      succeed (except for the very first one, which will use AXFR),
+	      and the zone will eventually expire.
+	      Therefore it should not be used in general.
+	      Still, in some special cases the use of this value may
+	      make sense.  For example, if the operator is sure that
+	      the master server supports IXFR and the zone is very
+	      large, they may want to avoid falling back to AXFR as
+	      it can be more expensive.
+	    </simpara>
+	  </listitem>
+	</varlistentry>
+      </variablelist>
+
+      <note>
+        <simpara>
+	  There used to be a boolean configuration item named
+	  <varname>use_ixfr</varname>.
+	  It was deprecated for the finer control described above.
+	  The <varname>request_ixfr</varname> item should be used instead.
+	</simpara>
+      </note>
 
 
-<!-- TODO: http://bind10.isc.org/ticket/1279 -->
-      <note><simpara>
-      One reason why IXFR is disabled by default in the current
-      release is because it does not support automatic fallback from IXFR to
-      AXFR when it encounters a primary server that doesn't support
-      outbound IXFR (and, not many existing implementations support
-      it).  Another, related reason is that it does not use AXFR even
-      if it has no knowledge about the zone (like at the very first
-      time the secondary server is set up).  IXFR requires the
-      "current version" of the zone, so obviously it doesn't work
-      in this situation and AXFR is the only workable choice.
-      The current release of <command>b10-xfrin</command> does not
-      make this selection automatically.
-      These features will be implemented in a near future
-      version, at which point we will enable IXFR by default.
-      </simpara></note>
     </section>
     </section>
 
 
 <!-- TODO:
 <!-- TODO:
@@ -2854,6 +2899,23 @@ what if a NOTIFY is sent?
 
 
         <screen>&gt; <userinput>Xfrin retransfer zone_name="<option>foo.example.org</option>" master=<option>192.0.2.99</option></userinput></screen>
         <screen>&gt; <userinput>Xfrin retransfer zone_name="<option>foo.example.org</option>" master=<option>192.0.2.99</option></userinput></screen>
       </para>
       </para>
+
+      <para>
+	The <command>retransfer</command> command always uses AXFR.
+	To use IXFR for a zone that has already been transferred once,
+	use the <command>refresh</command> command.
+	It honors the <varname>Xfrin/zones/request_ixfr</varname>
+	configuration item (see <xref linkend="request_ixfr"/>.), and
+	if it's configured to use IXFR, it will be used.
+      </para>
+
+      <para>
+	Both the <command>retransfer</command>
+	and <command>refresh</command> commands can be used for
+	an initial transfer before setting up secondary
+	configurations.
+	In this case AXFR will be used for the obvious reason.
+      </para>
     </section>
     </section>
 
 
     <section>
     <section>
@@ -2879,7 +2941,6 @@ http://bind10.isc.org/wiki/ScalableZoneLoadDesign#a7.2UpdatingaZone
       </para>
       </para>
     </section>
     </section>
 
 
-<!-- TODO: can that retransfer be used to identify a new zone? -->
 <!-- TODO: what if doesn't exist at that master IP? -->
 <!-- TODO: what if doesn't exist at that master IP? -->
 
 
   </chapter>
   </chapter>

+ 6 - 0
src/bin/auth/tests/query_unittest.cc

@@ -80,6 +80,12 @@ public:
                 return (FindResult());
                 return (FindResult());
         }
         }
     }
     }
+    virtual ConstZoneTableAccessorPtr
+    getZoneTableAccessor(const std::string&, bool) const {
+        isc_throw(isc::NotImplemented,
+                  "getZoneTableAccessor not implemented for SingletonList");
+    }
+
 private:
 private:
     DataSourceClient& client_;
     DataSourceClient& client_;
 };
 };

+ 1 - 1
src/bin/xfrin/b10-xfrin.xml

@@ -111,7 +111,7 @@ in separate zonemgr process.
       <varname>class</varname> (defaults to <quote>IN</quote>),
       <varname>class</varname> (defaults to <quote>IN</quote>),
       <varname>master_addr</varname> (the zone master to transfer from),
       <varname>master_addr</varname> (the zone master to transfer from),
       <varname>master_port</varname> (defaults to 53),
       <varname>master_port</varname> (defaults to 53),
-      <varname>use_ixfr</varname> (defaults to false), and
+      <varname>request_ixfr</varname> (defaults to yes), and
       <varname>tsig_key</varname> (optional TSIG key name to use).
       <varname>tsig_key</varname> (optional TSIG key name to use).
       The <varname>tsig_key</varname> is specified using a name that
       The <varname>tsig_key</varname> is specified using a name that
       corresponds to one of the TSIG keys configured in the global
       corresponds to one of the TSIG keys configured in the global

+ 179 - 77
src/bin/xfrin/tests/xfrin_test.py

@@ -129,17 +129,12 @@ class XfrinTestException(Exception):
 class XfrinTestTimeoutException(Exception):
 class XfrinTestTimeoutException(Exception):
     pass
     pass
 
 
-class MockCC(MockModuleCCSession):
-    def get_default_value(self, identifier):
-        # The returned values should be identical to the spec file
-        # XXX: these should be retrieved from the spec file
-        # (see MyCCSession of xfrout_test.py.in)
-        if identifier == "zones/master_port":
-            return TEST_MASTER_PORT
-        if identifier == "zones/class":
-            return TEST_RRCLASS_STR
-        if identifier == "zones/use_ixfr":
-            return False
+class MockCC(MockModuleCCSession, ConfigData):
+    def __init__(self):
+        super().__init__()
+        module_spec = isc.config.module_spec_from_file(
+            xfrin.SPECFILE_LOCATION)
+        ConfigData.__init__(self, module_spec)
 
 
     def add_remote_config_by_name(self, name, callback):
     def add_remote_config_by_name(self, name, callback):
         pass
         pass
@@ -242,6 +237,10 @@ class MockDataSourceClient():
         self.committed_diffs.append(self.diffs)
         self.committed_diffs.append(self.diffs)
         self.diffs = []
         self.diffs = []
 
 
+    def create_zone(self, zone_name):
+        # pretend it just succeeds
+        pass
+
 class MockXfrin(Xfrin):
 class MockXfrin(Xfrin):
     # This is a class attribute of a callable object that specifies a non
     # This is a class attribute of a callable object that specifies a non
     # default behavior triggered in _cc_check_command().  Specific test methods
     # default behavior triggered in _cc_check_command().  Specific test methods
@@ -265,21 +264,21 @@ class MockXfrin(Xfrin):
             MockXfrin.check_command_hook()
             MockXfrin.check_command_hook()
 
 
     def xfrin_start(self, zone_name, rrclass, db_file, master_addrinfo,
     def xfrin_start(self, zone_name, rrclass, db_file, master_addrinfo,
-                    tsig_key, request_type, check_soa=True):
+                    tsig_key, request_ixfr, check_soa=True):
         # store some of the arguments for verification, then call this
         # store some of the arguments for verification, then call this
         # method in the superclass
         # method in the superclass
         self.xfrin_started_master_addr = master_addrinfo[2][0]
         self.xfrin_started_master_addr = master_addrinfo[2][0]
         self.xfrin_started_master_port = master_addrinfo[2][1]
         self.xfrin_started_master_port = master_addrinfo[2][1]
-        self.xfrin_started_request_type = request_type
+        self.xfrin_started_request_ixfr = request_ixfr
         return Xfrin.xfrin_start(self, zone_name, rrclass, None,
         return Xfrin.xfrin_start(self, zone_name, rrclass, None,
                                  master_addrinfo, tsig_key,
                                  master_addrinfo, tsig_key,
-                                 request_type, check_soa)
+                                 request_ixfr, check_soa)
 
 
 class MockXfrinConnection(XfrinConnection):
 class MockXfrinConnection(XfrinConnection):
     def __init__(self, sock_map, zone_name, rrclass, datasrc_client,
     def __init__(self, sock_map, zone_name, rrclass, datasrc_client,
                  shutdown_event, master_addr, tsig_key=None):
                  shutdown_event, master_addr, tsig_key=None):
         super().__init__(sock_map, zone_name, rrclass, MockDataSourceClient(),
         super().__init__(sock_map, zone_name, rrclass, MockDataSourceClient(),
-                         shutdown_event, master_addr, TEST_DB_FILE)
+                         shutdown_event, master_addr, begin_soa_rrset)
         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
@@ -846,7 +845,9 @@ class TestXfrinConnection(unittest.TestCase):
 
 
         '''
         '''
         self.conn._zone_name = zone_name
         self.conn._zone_name = zone_name
-        self.conn._zone_soa = self.conn._get_zone_soa()
+        self.conn._zone_soa = xfrin._get_zone_soa(self.conn._datasrc_client,
+                                                  zone_name,
+                                                  self.conn._rrclass)
 
 
 class TestAXFR(TestXfrinConnection):
 class TestAXFR(TestXfrinConnection):
     def setUp(self):
     def setUp(self):
@@ -970,7 +971,9 @@ class TestAXFR(TestXfrinConnection):
                           RRType.IXFR)
                           RRType.IXFR)
 
 
         self._set_test_zone(Name('dup-soa.example'))
         self._set_test_zone(Name('dup-soa.example'))
-        self.conn._zone_soa = self.conn._get_zone_soa()
+        self.conn._zone_soa = xfrin._get_zone_soa(self.conn._datasrc_client,
+                                                  self.conn._zone_name,
+                                                  self.conn._rrclass)
         self.assertRaises(XfrinException, self.conn._create_query,
         self.assertRaises(XfrinException, self.conn._create_query,
                           RRType.IXFR)
                           RRType.IXFR)
 
 
@@ -2411,7 +2414,7 @@ class TestXfrinProcess(unittest.TestCase):
         # Normal, successful case.  We only check that things are cleaned up
         # Normal, successful case.  We only check that things are cleaned up
         # at the tearDown time.
         # at the tearDown time.
         process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
         process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
-                      self.master,  False, None, RRType.AXFR,
+                      self.master,  False, None, ZoneInfo.REQUEST_IXFR_DISABLED,
                       self.create_xfrinconn)
                       self.create_xfrinconn)
 
 
     def test_process_xfrin_exception_on_connect(self):
     def test_process_xfrin_exception_on_connect(self):
@@ -2419,7 +2422,7 @@ class TestXfrinProcess(unittest.TestCase):
         # cleaned up.
         # cleaned up.
         self.do_raise_on_connect = True
         self.do_raise_on_connect = True
         process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
         process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
-                      self.master,  False, None, RRType.AXFR,
+                      self.master,  False, None, ZoneInfo.REQUEST_IXFR_DISABLED,
                       self.create_xfrinconn)
                       self.create_xfrinconn)
 
 
     def test_process_xfrin_exception_on_close(self):
     def test_process_xfrin_exception_on_close(self):
@@ -2429,7 +2432,7 @@ class TestXfrinProcess(unittest.TestCase):
         self.do_raise_on_connect = True
         self.do_raise_on_connect = True
         self.do_raise_on_close = True
         self.do_raise_on_close = True
         process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
         process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
-                      self.master,  False, None, RRType.AXFR,
+                      self.master,  False, None, ZoneInfo.REQUEST_IXFR_DISABLED,
                       self.create_xfrinconn)
                       self.create_xfrinconn)
 
 
     def test_process_xfrin_exception_on_publish(self):
     def test_process_xfrin_exception_on_publish(self):
@@ -2437,7 +2440,7 @@ class TestXfrinProcess(unittest.TestCase):
         # everything must still be cleaned up.
         # everything must still be cleaned up.
         self.do_raise_on_publish = True
         self.do_raise_on_publish = True
         process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
         process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
-                      self.master,  False, None, RRType.AXFR,
+                      self.master,  False, None, ZoneInfo.REQUEST_IXFR_DISABLED,
                       self.create_xfrinconn)
                       self.create_xfrinconn)
 
 
 class TestXfrin(unittest.TestCase):
 class TestXfrin(unittest.TestCase):
@@ -2537,7 +2540,8 @@ class TestXfrin(unittest.TestCase):
         self.assertEqual(self.args['master'], self.xfr.xfrin_started_master_addr)
         self.assertEqual(self.args['master'], self.xfr.xfrin_started_master_addr)
         self.assertEqual(int(self.args['port']), self.xfr.xfrin_started_master_port)
         self.assertEqual(int(self.args['port']), self.xfr.xfrin_started_master_port)
         # By default we use AXFR (for now)
         # By default we use AXFR (for now)
-        self.assertEqual(RRType.AXFR, self.xfr.xfrin_started_request_type)
+        self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+                         self.xfr.xfrin_started_request_ixfr)
 
 
     def test_command_handler_retransfer_short_command1(self):
     def test_command_handler_retransfer_short_command1(self):
         # try it when only specifying the zone name (of unknown zone)
         # try it when only specifying the zone name (of unknown zone)
@@ -2650,8 +2654,9 @@ class TestXfrin(unittest.TestCase):
                          self.xfr.xfrin_started_master_addr)
                          self.xfr.xfrin_started_master_addr)
         self.assertEqual(int(TEST_MASTER_PORT),
         self.assertEqual(int(TEST_MASTER_PORT),
                          self.xfr.xfrin_started_master_port)
                          self.xfr.xfrin_started_master_port)
-        # By default we use AXFR (for now)
-        self.assertEqual(RRType.AXFR, self.xfr.xfrin_started_request_type)
+        # By default we use IXFR (with AXFR fallback)
+        self.assertEqual(ZoneInfo.REQUEST_IXFR_FIRST,
+                         self.xfr.xfrin_started_request_ixfr)
 
 
     def test_command_handler_notify(self):
     def test_command_handler_notify(self):
         # at this level, refresh is no different than retransfer.
         # at this level, refresh is no different than retransfer.
@@ -2734,12 +2739,19 @@ class TestXfrin(unittest.TestCase):
                                  Name(zone_config['tsig_key']).to_text())
                                  Name(zone_config['tsig_key']).to_text())
             else:
             else:
                 self.assertIsNone(zone_info.tsig_key_name)
                 self.assertIsNone(zone_info.tsig_key_name)
-            if 'use_ixfr' in zone_config and\
-               zone_config.get('use_ixfr'):
-                self.assertTrue(zone_info.use_ixfr)
-            else:
-                # if not set, should default to False
-                self.assertFalse(zone_info.use_ixfr)
+            if ('request_ixfr' in zone_config and
+                zone_config.get('request_ixfr')):
+                cfg_val = zone_config.get('request_ixfr')
+                val = zone_info.request_ixfr
+                if cfg_val == 'yes':
+                    self.assertEqual(ZoneInfo.REQUEST_IXFR_FIRST, val)
+                elif cfg_val == 'no':
+                    self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED, val)
+                elif cfg_val == 'only':
+                    self.assertEqual(ZoneInfo.REQUEST_IXFR_ONLY, val)
+            else:               # check the default
+                self.assertEqual(ZoneInfo.REQUEST_IXFR_FIRST,
+                                 zone_info.request_ixfr)
 
 
     def test_config_handler_zones(self):
     def test_config_handler_zones(self):
         # This test passes a number of good and bad configs, and checks whether
         # This test passes a number of good and bad configs, and checks whether
@@ -2751,7 +2763,7 @@ class TestXfrin(unittest.TestCase):
                    { 'name': 'test.example.',
                    { 'name': 'test.example.',
                     'master_addr': '192.0.2.1',
                     'master_addr': '192.0.2.1',
                     'master_port': 53,
                     'master_port': 53,
-                    'use_ixfr': False
+                    'request_ixfr': 'yes'
                    }
                    }
                  ]}
                  ]}
         self.assertEqual(self.xfr.config_handler(config1)['result'][0], 0)
         self.assertEqual(self.xfr.config_handler(config1)['result'][0], 0)
@@ -2763,12 +2775,24 @@ class TestXfrin(unittest.TestCase):
                     'master_addr': '192.0.2.2',
                     'master_addr': '192.0.2.2',
                     'master_port': 53,
                     'master_port': 53,
                     'tsig_key': "example.com:SFuWd/q99SzF8Yzd1QbB9g==",
                     'tsig_key': "example.com:SFuWd/q99SzF8Yzd1QbB9g==",
-                    'use_ixfr': True
+                    'request_ixfr': 'no'
                    }
                    }
                  ]}
                  ]}
         self.assertEqual(self.xfr.config_handler(config2)['result'][0], 0)
         self.assertEqual(self.xfr.config_handler(config2)['result'][0], 0)
         self._check_zones_config(config2)
         self._check_zones_config(config2)
 
 
+        config3 = {'transfers_in': 4,
+                   'zones': [
+                   { 'name': 'test.example.',
+                    'master_addr': '192.0.2.2',
+                    'master_port': 53,
+                    'tsig_key': "example.com:SFuWd/q99SzF8Yzd1QbB9g==",
+                    'request_ixfr': 'only'
+                   }
+                 ]}
+        self.assertEqual(self.xfr.config_handler(config3)['result'][0], 0)
+        self._check_zones_config(config3)
+
         # test that configuring the zone multiple times fails
         # test that configuring the zone multiple times fails
         zones = { 'transfers_in': 5,
         zones = { 'transfers_in': 5,
                   'zones': [
                   'zones': [
@@ -2783,7 +2807,7 @@ class TestXfrin(unittest.TestCase):
                 ]}
                 ]}
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         # since this has failed, we should still have the previous config
         # since this has failed, we should still have the previous config
-        self._check_zones_config(config2)
+        self._check_zones_config(config3)
 
 
         zones = { 'zones': [
         zones = { 'zones': [
                   { 'name': 'test.example.',
                   { 'name': 'test.example.',
@@ -2793,7 +2817,7 @@ class TestXfrin(unittest.TestCase):
                   }
                   }
                 ]}
                 ]}
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
-        self._check_zones_config(config2)
+        self._check_zones_config(config3)
 
 
         zones = { 'zones': [
         zones = { 'zones': [
                   { 'master_addr': '192.0.2.4',
                   { 'master_addr': '192.0.2.4',
@@ -2802,7 +2826,7 @@ class TestXfrin(unittest.TestCase):
                 ]}
                 ]}
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         # since this has failed, we should still have the previous config
         # since this has failed, we should still have the previous config
-        self._check_zones_config(config2)
+        self._check_zones_config(config3)
 
 
         zones = { 'zones': [
         zones = { 'zones': [
                   { 'name': 'bad..zone.',
                   { 'name': 'bad..zone.',
@@ -2812,7 +2836,7 @@ class TestXfrin(unittest.TestCase):
                 ]}
                 ]}
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         # since this has failed, we should still have the previous config
         # since this has failed, we should still have the previous config
-        self._check_zones_config(config2)
+        self._check_zones_config(config3)
 
 
         zones = { 'zones': [
         zones = { 'zones': [
                   { 'name': '',
                   { 'name': '',
@@ -2822,7 +2846,7 @@ class TestXfrin(unittest.TestCase):
                 ]}
                 ]}
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         # since this has failed, we should still have the previous config
         # since this has failed, we should still have the previous config
-        self._check_zones_config(config2)
+        self._check_zones_config(config3)
 
 
         zones = { 'zones': [
         zones = { 'zones': [
                   { 'name': 'test.example',
                   { 'name': 'test.example',
@@ -2832,7 +2856,7 @@ class TestXfrin(unittest.TestCase):
                 ]}
                 ]}
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         # since this has failed, we should still have the previous config
         # since this has failed, we should still have the previous config
-        self._check_zones_config(config2)
+        self._check_zones_config(config3)
 
 
         zones = { 'zones': [
         zones = { 'zones': [
                   { 'name': 'test.example',
                   { 'name': 'test.example',
@@ -2842,7 +2866,7 @@ class TestXfrin(unittest.TestCase):
                 ]}
                 ]}
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         # since this has failed, we should still have the previous config
         # since this has failed, we should still have the previous config
-        self._check_zones_config(config2)
+        self._check_zones_config(config3)
 
 
         zones = { 'zones': [
         zones = { 'zones': [
                   { 'name': 'test.example',
                   { 'name': 'test.example',
@@ -2854,7 +2878,7 @@ class TestXfrin(unittest.TestCase):
                 ]}
                 ]}
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         # since this has failed, we should still have the previous config
         # since this has failed, we should still have the previous config
-        self._check_zones_config(config2)
+        self._check_zones_config(config3)
 
 
         # let's also add a zone that is correct too, and make sure
         # let's also add a zone that is correct too, and make sure
         # that the new config is not partially taken
         # that the new config is not partially taken
@@ -2871,81 +2895,111 @@ class TestXfrin(unittest.TestCase):
                 ]}
                 ]}
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
         # since this has failed, we should still have the previous config
         # since this has failed, we should still have the previous config
-        self._check_zones_config(config2)
+        self._check_zones_config(config3)
+
+        # invalid request_ixfr value
+        zones = { 'zones': [
+                  { 'name': 'test.example',
+                    'master_addr': '192.0.2.7',
+                    'request_ixfr': 'bad value'
+                  }
+                ]}
+        self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
+        # since this has failed, we should still have the previous config
+        self._check_zones_config(config3)
 
 
     def test_config_handler_zones_default(self):
     def test_config_handler_zones_default(self):
         # Checking it some default config values apply.  Using a separate
         # Checking it some default config values apply.  Using a separate
         # test case for a fresh xfr object.
         # test case for a fresh xfr object.
         config = { 'zones': [
         config = { 'zones': [
                    { 'name': 'test.example.',
                    { 'name': 'test.example.',
-                    'master_addr': '192.0.2.1',
-                    'master_port': 53,
+                     'master_addr': '192.0.2.1',
+                     'master_port': 53,
                    }
                    }
                  ]}
                  ]}
         self.assertEqual(self.xfr.config_handler(config)['result'][0], 0)
         self.assertEqual(self.xfr.config_handler(config)['result'][0], 0)
         self._check_zones_config(config)
         self._check_zones_config(config)
 
 
-    def common_ixfr_setup(self, xfr_mode, use_ixfr, tsig_key_str = None):
+    def test_config_handler_use_ixfr(self):
+        # use_ixfr was deprecated and explicitly rejected for now.
+        config = { 'zones': [
+                   { 'name': 'test.example.',
+                     'master_addr': '192.0.2.1',
+                     'master_port': 53,
+                     'use_ixfr': True
+                   }
+                 ]}
+        self.assertEqual(self.xfr.config_handler(config)['result'][0], 1)
+
+    def common_ixfr_setup(self, xfr_mode, request_ixfr, tsig_key_str=None):
         # This helper method explicitly sets up a zone configuration with
         # This helper method explicitly sets up a zone configuration with
-        # use_ixfr, and invokes either retransfer or refresh.
+        # request_ixfr, and invokes either retransfer or refresh.
         # Shared by some of the following test cases.
         # Shared by some of the following test cases.
         config = {'zones': [
         config = {'zones': [
                 {'name': 'example.com.',
                 {'name': 'example.com.',
                  'master_addr': '192.0.2.1',
                  'master_addr': '192.0.2.1',
                  'tsig_key': tsig_key_str,
                  'tsig_key': tsig_key_str,
-                 'use_ixfr': use_ixfr}]}
+                 'request_ixfr': request_ixfr}]}
         self.assertEqual(self.xfr.config_handler(config)['result'][0], 0)
         self.assertEqual(self.xfr.config_handler(config)['result'][0], 0)
         self.assertEqual(self.xfr.command_handler(xfr_mode,
         self.assertEqual(self.xfr.command_handler(xfr_mode,
                                                   self.args)['result'][0], 0)
                                                   self.args)['result'][0], 0)
 
 
     def test_command_handler_retransfer_ixfr_enabled(self):
     def test_command_handler_retransfer_ixfr_enabled(self):
-        # If IXFR is explicitly enabled in config, IXFR will be used
-        self.common_ixfr_setup('retransfer', True)
-        self.assertEqual(RRType.IXFR, self.xfr.xfrin_started_request_type)
+        # retransfer always uses AXFR (disabling IXFR), regardless of
+        # request_ixfr value
+        self.common_ixfr_setup('retransfer', 'yes')
+        self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+                         self.xfr.xfrin_started_request_ixfr)
 
 
     def test_command_handler_refresh_ixfr_enabled(self):
     def test_command_handler_refresh_ixfr_enabled(self):
-        # Same for refresh
-        self.common_ixfr_setup('refresh', True)
-        self.assertEqual(RRType.IXFR, self.xfr.xfrin_started_request_type)
+        # for refresh, it honors zone configuration if defined (the default
+        # case is covered in test_command_handler_refresh
+        self.common_ixfr_setup('refresh', 'no')
+        self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+                         self.xfr.xfrin_started_request_ixfr)
 
 
     def test_command_handler_retransfer_with_tsig(self):
     def test_command_handler_retransfer_with_tsig(self):
-        self.common_ixfr_setup('retransfer', False, 'example.com.key')
-        self.assertEqual(RRType.AXFR, self.xfr.xfrin_started_request_type)
+        self.common_ixfr_setup('retransfer', 'no', 'example.com.key')
+        self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+                         self.xfr.xfrin_started_request_ixfr)
 
 
     def test_command_handler_retransfer_with_tsig_bad_key(self):
     def test_command_handler_retransfer_with_tsig_bad_key(self):
         # bad keys should not reach xfrin, but should they somehow,
         # bad keys should not reach xfrin, but should they somehow,
         # they are ignored (and result in 'key not found' + error log).
         # they are ignored (and result in 'key not found' + error log).
         self.assertRaises(XfrinZoneInfoException, self.common_ixfr_setup,
         self.assertRaises(XfrinZoneInfoException, self.common_ixfr_setup,
-                          'retransfer', False, 'bad.key')
+                          'retransfer', 'no', 'bad.key')
 
 
     def test_command_handler_retransfer_with_tsig_unknown_key(self):
     def test_command_handler_retransfer_with_tsig_unknown_key(self):
         self.assertRaises(XfrinZoneInfoException, self.common_ixfr_setup,
         self.assertRaises(XfrinZoneInfoException, self.common_ixfr_setup,
-                          'retransfer', False, 'no.such.key')
+                          'retransfer', 'no', 'no.such.key')
 
 
     def test_command_handler_refresh_with_tsig(self):
     def test_command_handler_refresh_with_tsig(self):
-        self.common_ixfr_setup('refresh', False, 'example.com.key')
-        self.assertEqual(RRType.AXFR, self.xfr.xfrin_started_request_type)
+        self.common_ixfr_setup('refresh', 'no', 'example.com.key')
+        self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+                         self.xfr.xfrin_started_request_ixfr)
 
 
     def test_command_handler_refresh_with_tsig_bad_key(self):
     def test_command_handler_refresh_with_tsig_bad_key(self):
         # bad keys should not reach xfrin, but should they somehow,
         # bad keys should not reach xfrin, but should they somehow,
         # they are ignored (and result in 'key not found' + error log).
         # they are ignored (and result in 'key not found' + error log).
         self.assertRaises(XfrinZoneInfoException, self.common_ixfr_setup,
         self.assertRaises(XfrinZoneInfoException, self.common_ixfr_setup,
-                          'refresh', False, 'bad.key')
+                          'refresh', 'no', 'bad.key')
 
 
     def test_command_handler_refresh_with_tsig_unknown_key(self):
     def test_command_handler_refresh_with_tsig_unknown_key(self):
         self.assertRaises(XfrinZoneInfoException, self.common_ixfr_setup,
         self.assertRaises(XfrinZoneInfoException, self.common_ixfr_setup,
-                          'refresh', False, 'no.such.key')
+                          'refresh', 'no', 'no.such.key')
 
 
     def test_command_handler_retransfer_ixfr_disabled(self):
     def test_command_handler_retransfer_ixfr_disabled(self):
         # Similar to the previous case, but explicitly disabled.  AXFR should
         # Similar to the previous case, but explicitly disabled.  AXFR should
         # be used.
         # be used.
-        self.common_ixfr_setup('retransfer', False)
-        self.assertEqual(RRType.AXFR, self.xfr.xfrin_started_request_type)
+        self.common_ixfr_setup('retransfer', 'no')
+        self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+                         self.xfr.xfrin_started_request_ixfr)
 
 
     def test_command_handler_refresh_ixfr_disabled(self):
     def test_command_handler_refresh_ixfr_disabled(self):
         # Same for refresh
         # Same for refresh
-        self.common_ixfr_setup('refresh', False)
-        self.assertEqual(RRType.AXFR, self.xfr.xfrin_started_request_type)
+        self.common_ixfr_setup('refresh', 'no')
+        self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+                         self.xfr.xfrin_started_request_ixfr)
 
 
 class TestXfrinMemoryZones(unittest.TestCase):
 class TestXfrinMemoryZones(unittest.TestCase):
     def setUp(self):
     def setUp(self):
@@ -3157,6 +3211,13 @@ class TestXfrinProcess(unittest.TestCase):
         self.__published = []
         self.__published = []
         # How many connections were created.
         # How many connections were created.
         self.__created_connections = 0
         self.__created_connections = 0
+        # prepare for possible replacement
+        self.__orig_get_zone_soa = xfrin._get_zone_soa
+        xfrin._get_zone_soa = lambda x, y, z: begin_soa_rdata
+
+    def tearDown(self):
+        # restore original value
+        xfrin._get_zone_soa = self.__orig_get_zone_soa
 
 
     def __get_connection(self, *args):
     def __get_connection(self, *args):
         """
         """
@@ -3212,7 +3273,7 @@ class TestXfrinProcess(unittest.TestCase):
         """
         """
         pass
         pass
 
 
-    def __do_test(self, rets, transfers, request_type):
+    def __do_test(self, rets, transfers, request_ixfr):
         """
         """
         Do the actual test. The request type, prepared sucesses/failures
         Do the actual test. The request type, prepared sucesses/failures
         and expected sequence of transfers is passed to specify what test
         and expected sequence of transfers is passed to specify what test
@@ -3221,8 +3282,8 @@ class TestXfrinProcess(unittest.TestCase):
         self.__rets = rets
         self.__rets = rets
         published = rets[-1]
         published = rets[-1]
         xfrin.process_xfrin(self, XfrinRecorder(), Name("example.org."),
         xfrin.process_xfrin(self, XfrinRecorder(), Name("example.org."),
-                            RRClass.IN, None, None, None, True, None,
-                            request_type, self.__get_connection)
+                            RRClass.IN, None, None, TEST_MASTER_IPV4_ADDRINFO,
+                            True, None, request_ixfr, self.__get_connection)
         self.assertEqual([], self.__rets)
         self.assertEqual([], self.__rets)
         self.assertEqual(transfers, self.__transfers)
         self.assertEqual(transfers, self.__transfers)
         # Create a connection for each attempt
         # Create a connection for each attempt
@@ -3233,7 +3294,7 @@ class TestXfrinProcess(unittest.TestCase):
         """
         """
         Everything OK the first time, over IXFR.
         Everything OK the first time, over IXFR.
         """
         """
-        self.__do_test([XFRIN_OK], [RRType.IXFR], RRType.IXFR)
+        self.__do_test([XFRIN_OK], [RRType.IXFR], ZoneInfo.REQUEST_IXFR_FIRST)
         # Check there was loadzone command
         # Check there was loadzone command
         self.assertTrue(self._send_cc_session.send_called)
         self.assertTrue(self._send_cc_session.send_called)
         self.assertTrue(self._send_cc_session.send_called_correctly)
         self.assertTrue(self._send_cc_session.send_called_correctly)
@@ -3244,23 +3305,27 @@ class TestXfrinProcess(unittest.TestCase):
         """
         """
         Everything OK the first time, over AXFR.
         Everything OK the first time, over AXFR.
         """
         """
-        self.__do_test([XFRIN_OK], [RRType.AXFR], RRType.AXFR)
+        self.__do_test([XFRIN_OK], [RRType.AXFR],
+                       ZoneInfo.REQUEST_IXFR_DISABLED)
 
 
     def test_axfr_fail(self):
     def test_axfr_fail(self):
         """
         """
         The transfer failed over AXFR. Should not be retried (we don't expect
         The transfer failed over AXFR. Should not be retried (we don't expect
-        to fail on AXFR, but succeed on IXFR and we didn't use IXFR in the first
-        place for some reason.
+        to fail on AXFR, but succeed on IXFR and we didn't use IXFR in the
+        first place for some reason.
+
         """
         """
-        self.__do_test([XFRIN_FAIL], [RRType.AXFR], RRType.AXFR)
+        self.__do_test([XFRIN_FAIL], [RRType.AXFR],
+                       ZoneInfo.REQUEST_IXFR_DISABLED)
 
 
     def test_ixfr_fallback(self):
     def test_ixfr_fallback(self):
         """
         """
-        The transfer fails over IXFR, but suceeds over AXFR. It should fall back
-        to it and say everything is OK.
+        The transfer fails over IXFR, but suceeds over AXFR. It should fall
+        back to it and say everything is OK.
+
         """
         """
         self.__do_test([XFRIN_FAIL, XFRIN_OK], [RRType.IXFR, RRType.AXFR],
         self.__do_test([XFRIN_FAIL, XFRIN_OK], [RRType.IXFR, RRType.AXFR],
-                       RRType.IXFR)
+                       ZoneInfo.REQUEST_IXFR_FIRST)
 
 
     def test_ixfr_fail(self):
     def test_ixfr_fail(self):
         """
         """
@@ -3268,18 +3333,55 @@ class TestXfrinProcess(unittest.TestCase):
         (only once) and should try both before giving up.
         (only once) and should try both before giving up.
         """
         """
         self.__do_test([XFRIN_FAIL, XFRIN_FAIL],
         self.__do_test([XFRIN_FAIL, XFRIN_FAIL],
-                       [RRType.IXFR, RRType.AXFR], RRType.IXFR)
+                       [RRType.IXFR, RRType.AXFR], ZoneInfo.REQUEST_IXFR_FIRST)
+
+    def test_ixfr_only(self):
+        """
+        The transfer fails and IXFR_ONLY is specified.  It shouldn't fall
+        back to AXFR and should report failure.
+        """
+        self.__do_test([XFRIN_FAIL], [RRType.IXFR], ZoneInfo.REQUEST_IXFR_ONLY)
 
 
     def test_send_loadzone(self):
     def test_send_loadzone(self):
         """
         """
         Check the loadzone command is sent after successful transfer.
         Check the loadzone command is sent after successful transfer.
         """
         """
-        self.__do_test([XFRIN_OK], [RRType.IXFR], RRType.IXFR)
+        self.__do_test([XFRIN_OK], [RRType.IXFR],
+                       ZoneInfo.REQUEST_IXFR_FIRST)
         self.assertTrue(self._send_cc_session.send_called)
         self.assertTrue(self._send_cc_session.send_called)
         self.assertTrue(self._send_cc_session.send_called_correctly)
         self.assertTrue(self._send_cc_session.send_called_correctly)
         self.assertTrue(self._send_cc_session.recv_called)
         self.assertTrue(self._send_cc_session.recv_called)
         self.assertTrue(self._send_cc_session.recv_called_correctly)
         self.assertTrue(self._send_cc_session.recv_called_correctly)
 
 
+    def test_initial_request_type(self):
+        """Check initial xfr reuqest type (AXFR or IXFR).
+
+        Varying the policy of use of IXFR and availability of current
+        zone SOA.  We are only interested in the initial request type,
+        so won't check the xfr results.
+
+        """
+        for soa in [begin_soa_rdata, None]:
+            for request_ixfr in [ZoneInfo.REQUEST_IXFR_FIRST,
+                                 ZoneInfo.REQUEST_IXFR_ONLY,
+                                 ZoneInfo.REQUEST_IXFR_DISABLED]:
+                # set up our dummy _get_zone_soa()
+                xfrin._get_zone_soa = lambda x, y, z: soa
+
+                # Clear all counters
+                self.__transfers = []
+                self.__published = []
+                self.__created_connections = 0
+
+                # Determine the expected type
+                expected_type = RRType.IXFR
+                if (soa is None or
+                    request_ixfr == ZoneInfo.REQUEST_IXFR_DISABLED):
+                    expected_type = RRType.AXFR
+
+                # perform the test
+                self.__do_test([XFRIN_OK], [expected_type], request_ixfr)
+
 class TestFormatting(unittest.TestCase):
 class TestFormatting(unittest.TestCase):
     # If the formatting functions are moved to a more general library
     # If the formatting functions are moved to a more general library
     # (ticket #1379), these tests should be moved with them.
     # (ticket #1379), these tests should be moved with them.

+ 283 - 161
src/bin/xfrin/xfrin.py.in

@@ -31,6 +31,7 @@ from isc.config.ccsession import *
 from isc.statistics import Counters
 from isc.statistics import Counters
 from isc.notify import notify_out
 from isc.notify import notify_out
 import isc.util.process
 import isc.util.process
+from isc.util.address_formatter import AddressFormatter
 from isc.datasrc import DataSourceClient, ZoneFinder
 from isc.datasrc import DataSourceClient, ZoneFinder
 import isc.net.parse
 import isc.net.parse
 from isc.xfrin.diff import Diff
 from isc.xfrin.diff import Diff
@@ -565,18 +566,25 @@ class XfrinConnection(asyncore.dispatcher):
 
 
     def __init__(self,
     def __init__(self,
                  sock_map, zone_name, rrclass, datasrc_client,
                  sock_map, zone_name, rrclass, datasrc_client,
-                 shutdown_event, master_addrinfo, db_file, tsig_key=None,
+                 shutdown_event, master_addrinfo, zone_soa, tsig_key=None,
                  idle_timeout=60):
                  idle_timeout=60):
-        '''Constructor of the XfirnConnection class.
+        """Constructor of the XfirnConnection class.
+
+        Parameters:
+          sock_map: empty dict, used with asyncore.
+          zone_name (dns.Name): Zone name.
+          rrclass (dns.RRClass): Zone RR class.
+          datasrc_client (DataSourceClient): the data source client object
+            used for the XFR session.
+          shutdown_event (threading.Event): used for synchronization with
+            parent thread.
+          master_addrinfo (tuple: (sock family, sock type, sockaddr)):
+            address and port of the master server.
+          zone_soa (RRset or None): SOA RRset of zone's current SOA or None
+            if it's not available.
+          idle_timeout (int): max idle time for read data from socket.
 
 
-        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.
-
-        '''
+        """
 
 
         asyncore.dispatcher.__init__(self, map=sock_map)
         asyncore.dispatcher.__init__(self, map=sock_map)
 
 
@@ -595,9 +603,8 @@ class XfrinConnection(asyncore.dispatcher):
         self._rrclass = rrclass
         self._rrclass = rrclass
 
 
         # Data source handler
         # Data source handler
-        self._db_file = db_file
         self._datasrc_client = datasrc_client
         self._datasrc_client = datasrc_client
-        self._zone_soa = self._get_zone_soa()
+        self._zone_soa = zone_soa
 
 
         self._sock_map = sock_map
         self._sock_map = sock_map
         self._soa_rr_count = 0
         self._soa_rr_count = 0
@@ -626,54 +633,6 @@ class XfrinConnection(asyncore.dispatcher):
         self.create_socket(self._master_addrinfo[0], self._master_addrinfo[1])
         self.create_socket(self._master_addrinfo[0], self._master_addrinfo[1])
         self.socket.setblocking(1)
         self.socket.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)
-        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):
     def __set_xfrstate(self, new_state):
         self.__state = new_state
         self.__state = new_state
 
 
@@ -746,8 +705,9 @@ class XfrinConnection(asyncore.dispatcher):
 
 
         msg = self._create_query(query_type)
         msg = self._create_query(query_type)
         render = MessageRenderer()
         render = MessageRenderer()
-        # XXX Currently, python wrapper doesn't accept 'None' parameter in this case,
-        # we should remove the if statement and use a universal interface later.
+        # XXX Currently, python wrapper doesn't accept 'None' parameter in this
+        # case, we should remove the if statement and use a universal
+        # interface later.
         if self._tsig_key is not None:
         if self._tsig_key is not None:
             self._tsig_ctx = self._tsig_ctx_creator(self._tsig_key)
             self._tsig_ctx = self._tsig_ctx_creator(self._tsig_key)
             msg.to_wire(render, self._tsig_ctx)
             msg.to_wire(render, self._tsig_ctx)
@@ -1114,16 +1074,95 @@ class XfrinConnection(asyncore.dispatcher):
 
 
         return False
         return False
 
 
+def _get_zone_soa(datasrc_client, zone_name, zone_class):
+    """Retrieve the current SOA RR of the zone to be transferred.
+
+    This function is essentially private to the module, but will also
+    be called (or tweaked) from tests; no one else should use this
+    function directly.
+
+    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.
+
+    """
+    # datasrc_client should never be None in production case (only tests could
+    # specify None)
+    if datasrc_client is None:
+        return None
+
+    # get the zone finder.  this must be SUCCESS (not even
+    # PARTIALMATCH) because we are specifying the zone origin name.
+    result, finder = datasrc_client.find_zone(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.
+        # For longer term, we should probably separate this level of zone
+        # management outside of xfrin.
+        datasrc_client.create_zone(zone_name)
+        logger.warn(XFRIN_ZONE_CREATED, format_zone_str(zone_name, zone_class))
+        # try again
+        result, finder = datasrc_client.find_zone(zone_name)
+    if result != DataSourceClient.SUCCESS:
+        return None
+    result, soa_rrset, _ = finder.find(zone_name, RRType.SOA)
+    if result != ZoneFinder.SUCCESS:
+        logger.info(XFRIN_ZONE_NO_SOA, format_zone_str(zone_name, zone_class))
+        return None
+    if soa_rrset.get_rdata_count() != 1:
+        logger.warn(XFRIN_ZONE_MULTIPLE_SOA,
+                    format_zone_str(zone_name, zone_class),
+                    soa_rrset.get_rdata_count())
+        return None
+    return soa_rrset
+
+def __get_initial_xfr_type(zone_soa, request_ixfr, zname, zclass, master_addr):
+    """Determine the initial xfr request type.
+
+    This is a dedicated subroutine of __process_xfrin.
+    """
+    if zone_soa is None:
+        # This is a kind of special case, so we log it at info level.
+        logger.info(XFRIN_INITIAL_AXFR, format_zone_str(zname, zclass),
+                    AddressFormatter(master_addr))
+        return RRType.AXFR
+    if request_ixfr == ZoneInfo.REQUEST_IXFR_DISABLED:
+        logger.debug(DBG_XFRIN_TRACE, XFRIN_INITIAL_IXFR_DISABLED,
+                     format_zone_str(zname, zclass),
+                     AddressFormatter(master_addr))
+        return RRType.AXFR
+
+    assert(request_ixfr == ZoneInfo.REQUEST_IXFR_FIRST or
+           request_ixfr == ZoneInfo.REQUEST_IXFR_ONLY)
+    logger.debug(DBG_XFRIN_TRACE, XFRIN_INITIAL_IXFR,
+                     format_zone_str(zname, zclass),
+                     AddressFormatter(master_addr))
+    return RRType.IXFR
+
 def __process_xfrin(server, zone_name, rrclass, db_file,
 def __process_xfrin(server, zone_name, rrclass, db_file,
                     shutdown_event, master_addrinfo, check_soa, tsig_key,
                     shutdown_event, master_addrinfo, check_soa, tsig_key,
-                    request_type, conn_class):
+                    request_ixfr, conn_class):
     conn = None
     conn = None
     exception = None
     exception = None
     ret = XFRIN_FAIL
     ret = XFRIN_FAIL
     try:
     try:
         # 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
-        # still assume an sqlite3-based data source, and use both the old and new
-        # data source APIs.  We also need to use a mock client for tests.
+        # still assume an sqlite3-based data source, and use both the old and
+        # new data source APIs.  We also need to use a mock client for tests.
         # For a temporary workaround to deal with these situations, we skip the
         # For a temporary workaround to deal with these situations, we skip the
         # creation when the given file is none (the test case).  Eventually
         # creation when the given file is none (the test case).  Eventually
         # this code will be much cleaner.
         # this code will be much cleaner.
@@ -1131,38 +1170,55 @@ def __process_xfrin(server, zone_name, rrclass, db_file,
         if db_file is not None:
         if db_file is not None:
             # temporary hardcoded sqlite initialization. Once we decide on
             # temporary hardcoded sqlite initialization. Once we decide on
             # the config specification, we need to update this (TODO)
             # the config specification, we need to update this (TODO)
-            # this may depend on #1207, or any follow-up ticket created for #1207
+            # this may depend on #1207, or any follow-up ticket created for
+            # #1207
             datasrc_type = "sqlite3"
             datasrc_type = "sqlite3"
             datasrc_config = "{ \"database_file\": \"" + db_file + "\"}"
             datasrc_config = "{ \"database_file\": \"" + db_file + "\"}"
             datasrc_client = DataSourceClient(datasrc_type, datasrc_config)
             datasrc_client = DataSourceClient(datasrc_type, datasrc_config)
 
 
-        # Create a TCP connection for the XFR session and perform the operation.
+        # Get the current zone SOA (if available) and determine the initial
+        # reuqest type: AXFR or IXFR.
+        zone_soa = _get_zone_soa(datasrc_client, zone_name, rrclass)
+        request_type = __get_initial_xfr_type(zone_soa, request_ixfr,
+                                              zone_name, rrclass,
+                                              master_addrinfo[2])
+
+        # Create a TCP connection for the XFR session and perform the
+        # operation.
         sock_map = {}
         sock_map = {}
-        # In case we were asked to do IXFR and that one fails, we try again with
-        # AXFR. But only if we could actually connect to the server.
+        # In case we were asked to do IXFR and that one fails, we try again
+        # with AXFR. But only if we could actually connect to the server.
         #
         #
-        # So we start with retry as True, which is set to false on each attempt.
-        # In the case of connected but failed IXFR, we set it to true once again.
+        # So we start with retry as True, which is set to false on each
+        # attempt. In the case of connected but failed IXFR, we set it to true
+        # once again.
         retry = True
         retry = True
         while retry:
         while retry:
             retry = False
             retry = False
             conn = conn_class(sock_map, zone_name, rrclass, datasrc_client,
             conn = conn_class(sock_map, zone_name, rrclass, datasrc_client,
-                              shutdown_event, master_addrinfo, db_file,
+                              shutdown_event, master_addrinfo, zone_soa,
                               tsig_key)
                               tsig_key)
             conn.init_socket()
             conn.init_socket()
             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)
                 if ret == XFRIN_FAIL and request_type == RRType.IXFR:
                 if ret == XFRIN_FAIL and request_type == RRType.IXFR:
-                    # IXFR failed for some reason. It might mean the server can't
-                    # handle it, or we don't have the zone or we are out of sync or
-                    # whatever else. So we retry with with AXFR, as it may succeed
-                    # in many such cases.
-                    retry = True
-                    request_type = RRType.AXFR
-                    logger.warn(XFRIN_XFR_TRANSFER_FALLBACK, conn.zone_str())
-                    conn.close()
-                    conn = None
+                    # IXFR failed for some reason. It might mean the server
+                    # can't handle it, or we don't have the zone or we are out
+                    # of sync or whatever else. So we retry with with AXFR, as
+                    # it may succeed in many such cases; if "IXFR only" is
+                    # specified in request_ixfr, however, we suppress the
+                    # fallback.
+                    if request_ixfr == ZoneInfo.REQUEST_IXFR_ONLY:
+                        logger.warn(XFRIN_XFR_TRANSFER_FALLBACK_DISABLED,
+                                    conn.zone_str())
+                    else:
+                        retry = True
+                        request_type = RRType.AXFR
+                        logger.warn(XFRIN_XFR_TRANSFER_FALLBACK,
+                                    conn.zone_str())
+                        conn.close()
+                        conn = None
 
 
     except Exception as ex:
     except Exception as ex:
         # If exception happens, just remember it here so that we can re-raise
         # If exception happens, just remember it here so that we can re-raise
@@ -1188,7 +1244,7 @@ def __process_xfrin(server, zone_name, rrclass, db_file,
 
 
 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, tsig_key,
                   shutdown_event, master_addrinfo, check_soa, tsig_key,
-                  request_type, conn_class=XfrinConnection):
+                  request_ixfr, conn_class=XfrinConnection):
     # Even if it should be rare, the main process of xfrin session can
     # Even if it should be rare, the main process of xfrin session can
     # raise an exception.  In order to make sure the lock in xfrin_recorder
     # raise an exception.  In order to make sure the lock in xfrin_recorder
     # is released in any cases, we delegate the main part to the helper
     # is released in any cases, we delegate the main part to the helper
@@ -1198,14 +1254,17 @@ def process_xfrin(server, xfrin_recorder, zone_name, rrclass, db_file,
     try:
     try:
         __process_xfrin(server, zone_name, rrclass, db_file,
         __process_xfrin(server, zone_name, rrclass, db_file,
                         shutdown_event, master_addrinfo, check_soa, tsig_key,
                         shutdown_event, master_addrinfo, check_soa, tsig_key,
-                        request_type, conn_class)
+                        request_ixfr, conn_class)
     except Exception as ex:
     except Exception as ex:
         # don't log it until we complete decrement().
         # don't log it until we complete decrement().
         exception = ex
         exception = ex
     xfrin_recorder.decrement(zone_name)
     xfrin_recorder.decrement(zone_name)
 
 
     if exception is not None:
     if exception is not None:
-        typestr = "AXFR" if request_type == RRType.AXFR else "IXFR"
+        if request_ixfr == ZoneInfo.REQUEST_IXFR_DISABLED:
+            typestr = "AXFR"
+        else:
+            typestr = "IXFR"
         logger.error(XFRIN_XFR_PROCESS_FAILURE, typestr, zone_name.to_text(),
         logger.error(XFRIN_XFR_PROCESS_FAILURE, typestr, zone_name.to_text(),
                      str(rrclass), str(exception))
                      str(rrclass), str(exception))
 
 
@@ -1238,10 +1297,26 @@ class XfrinRecorder:
         return ret
         return ret
 
 
 class ZoneInfo:
 class ZoneInfo:
+    # Internal values corresponding to request_ixfr
+    REQUEST_IXFR_FIRST = 0      # request_ixfr=yes, use IXFR 1st then AXFR
+    REQUEST_IXFR_ONLY = 1       # request_ixfr=only, use IXFR only
+    REQUEST_IXFR_DISABLED = 2   # request_ixfr=no, AXFR-only
+
+    # Map from configuration values for request_ixfr to internal values
+    # This is a constant; don't modify.
+    REQUEST_IXFR_CFG_TO_VAL = { 'yes': REQUEST_IXFR_FIRST,
+                                'only': REQUEST_IXFR_ONLY,
+                                'no': REQUEST_IXFR_DISABLED }
+
     def __init__(self, config_data, module_cc):
     def __init__(self, config_data, module_cc):
         """Creates a zone_info with the config data element as
         """Creates a zone_info with the config data element as
            specified by the 'zones' list in xfrin.spec. Module_cc is
            specified by the 'zones' list in xfrin.spec. Module_cc is
            needed to get the defaults from the specification"""
            needed to get the defaults from the specification"""
+        # Handle deprecated config parameter explicitly for the moment.
+        if config_data.get('use_ixfr') is not None:
+            raise XfrinZoneInfoException('use_ixfr was deprecated.' +
+                                         'use rquest_ixfr')
+
         self._module_cc = module_cc
         self._module_cc = module_cc
         self.set_name(config_data.get('name'))
         self.set_name(config_data.get('name'))
         self.set_master_addr(config_data.get('master_addr'))
         self.set_master_addr(config_data.get('master_addr'))
@@ -1249,7 +1324,17 @@ class ZoneInfo:
         self.set_master_port(config_data.get('master_port'))
         self.set_master_port(config_data.get('master_port'))
         self.set_zone_class(config_data.get('class'))
         self.set_zone_class(config_data.get('class'))
         self.set_tsig_key_name(config_data.get('tsig_key'))
         self.set_tsig_key_name(config_data.get('tsig_key'))
-        self.set_use_ixfr(config_data.get('use_ixfr'))
+        self.set_request_ixfr(config_data.get('request_ixfr'))
+
+    @property
+    def request_ixfr(self):
+        """Policy on the use of IXFR.
+
+        Possible values are REQUEST_IXFR_xxx, internally stored in
+        __request_ixfr, read-only outside of the class.
+
+        """
+        return self.__request_ixfr
 
 
     def set_name(self, name_str):
     def set_name(self, name_str):
         """Set the name for this zone given a name string.
         """Set the name for this zone given a name string.
@@ -1336,16 +1421,15 @@ class ZoneInfo:
         else:
         else:
             return key
             return key
 
 
-    def set_use_ixfr(self, use_ixfr):
-        """Set use_ixfr. If set to True, it will use
-           IXFR for incoming transfers. If set to False, it will use AXFR.
-           At this moment there is no automatic fallback"""
-        # TODO: http://bind10.isc.org/ticket/1279
-        if use_ixfr is None:
-            self.use_ixfr = \
-                self._module_cc.get_default_value("zones/use_ixfr")
-        else:
-            self.use_ixfr = use_ixfr
+    def set_request_ixfr(self, request_ixfr):
+        if request_ixfr is None:
+            request_ixfr = \
+                self._module_cc.get_default_value("zones/request_ixfr")
+        try:
+            self.__request_ixfr = self.REQUEST_IXFR_CFG_TO_VAL[request_ixfr]
+        except KeyError:
+            raise XfrinZoneInfoException('invalid value for request_ixfr: ' +
+                                         request_ixfr)
 
 
     def get_master_addr_info(self):
     def get_master_addr_info(self):
         return (self.master_addr.family, socket.SOCK_STREAM,
         return (self.master_addr.family, socket.SOCK_STREAM,
@@ -1518,6 +1602,86 @@ class Xfrin:
                 continue
                 continue
             th.join()
             th.join()
 
 
+    def __validate_notify_addr(self, notify_addr, zone_str, zone_info):
+        """Validate notify source as a destination for xfr source.
+
+        This is called from __handle_xfr_command in case xfr is triggered
+        by ZoneMgr either due to incoming Notify or periodic refresh event.
+
+        """
+        if zone_info is None:
+            # TODO what to do? no info known about zone. defaults?
+            errmsg = "Got notification to retransfer unknown zone " + zone_str
+            logger.info(XFRIN_RETRANSFER_UNKNOWN_ZONE, zone_str)
+            return create_answer(1, errmsg)
+        else:
+            master_addr = zone_info.get_master_addr_info()
+            if (notify_addr[0] != master_addr[0] or
+                notify_addr[2] != master_addr[2]):
+                notify_addr_str = format_addrinfo(notify_addr)
+                master_addr_str = format_addrinfo(master_addr)
+                errmsg = "Got notification for " + zone_str\
+                    + "from unknown address: " + notify_addr_str;
+                logger.info(XFRIN_NOTIFY_UNKNOWN_MASTER, zone_str,
+                            notify_addr_str, master_addr_str)
+                return create_answer(1, errmsg)
+
+        # Notified address is okay
+        return None
+
+    def __get_running_request_ixfr(self, arg_request_ixfr, zone_info):
+        """Determine the request_ixfr policy for a specific transfer.
+
+        This is a dedicated subroutine of __handle_xfr_command.
+
+        """
+        # If explicitly specified, use it.
+        if arg_request_ixfr is not None:
+            return arg_request_ixfr
+        # Otherwise, if zone info is known, use its value.
+        if zone_info is not None:
+            return zone_info.request_ixfr
+        # Otherwise, use the default value for ZoneInfo
+        request_ixfr_def = \
+            self._module_cc.get_default_value("zones/request_ixfr")
+        return ZoneInfo.REQUEST_IXFR_CFG_TO_VAL[request_ixfr_def]
+
+    def __handle_xfr_command(self, args, arg_db, check_soa, addr_validator,
+                             request_ixfr):
+        """Common subroutine for handling transfer commands.
+
+        This helper method unifies both cases of transfer command from
+        ZoneMgr or from a user.  Depending on who invokes the transfer,
+        details of validation and parameter selection slightly vary.
+        These conditions are passed through parameters and handled in the
+        unified code of this method accordingly.
+
+        If this is from the ZoneMgr due to incoming notify, zone transfer
+        should start from the notify's source address as long as it's
+        configured as a master address, according to RFC1996.  The current
+        implementation conforms to it in a limited way: we can only set one
+        master address. Once we add the ability to have multiple master
+        addresses, we should check if it matches one of them, and then use it.
+
+        In case of transfer command from the user, if the command specifies
+        the master address, use that one; otherwise try to use a configured
+        master address for the zone.
+
+        """
+        (zone_name, rrclass) = self._parse_zone_name_and_class(args)
+        master_addr = self._parse_master_and_port(args, zone_name, rrclass)
+        zone_info = self._get_zone_info(zone_name, rrclass)
+        tsig_key = None if zone_info is None else zone_info.get_tsig_key()
+        db_file = arg_db or self._get_db_file()
+        zone_str = format_zone_str(zone_name, rrclass) # for logging
+        answer = addr_validator(master_addr, zone_str, zone_info)
+        if answer is not None:
+            return answer
+        request_ixfr = self.__get_running_request_ixfr(request_ixfr, zone_info)
+        ret = self.xfrin_start(zone_name, rrclass, db_file, master_addr,
+                               tsig_key, request_ixfr, check_soa)
+        return create_answer(ret[0], ret[1])
+
     def command_handler(self, command, args):
     def command_handler(self, command, args):
         logger.debug(DBG_XFRIN_TRACE, XFRIN_RECEIVED_COMMAND, command)
         logger.debug(DBG_XFRIN_TRACE, XFRIN_RECEIVED_COMMAND, command)
         answer = create_answer(0)
         answer = create_answer(0)
@@ -1525,69 +1689,26 @@ class Xfrin:
             if command == 'shutdown':
             if command == 'shutdown':
                 self._shutdown_event.set()
                 self._shutdown_event.set()
             elif command == 'notify' or command == REFRESH_FROM_ZONEMGR:
             elif command == 'notify' or command == REFRESH_FROM_ZONEMGR:
-                # Xfrin receives the refresh/notify command from zone manager.
-                # notify command maybe has the parameters which
-                # specify the notifyfrom address and port, according the RFC1996, zone
-                # transfer should starts first from the notifyfrom, but now, let 'TODO' it.
-                # (using the value now, while we can only set one master address, would be
-                # a security hole. Once we add the ability to have multiple master addresses,
-                # we should check if it matches one of them, and then use it.)
-                (zone_name, rrclass) = self._parse_zone_name_and_class(args)
-                zone_str = format_zone_str(zone_name, rrclass)
-                zone_info = self._get_zone_info(zone_name, rrclass)
-                notify_addr = self._parse_master_and_port(args, zone_name,
-                                                          rrclass)
-                if zone_info is None:
-                    # TODO what to do? no info known about zone. defaults?
-                    errmsg = "Got notification to retransfer unknown zone " + zone_str
-                    logger.info(XFRIN_RETRANSFER_UNKNOWN_ZONE, zone_str)
-                    answer = create_answer(1, errmsg)
-                else:
-                    request_type = RRType.AXFR
-                    if zone_info.use_ixfr:
-                        request_type = RRType.IXFR
-                    master_addr = zone_info.get_master_addr_info()
-                    if notify_addr[0] == master_addr[0] and\
-                       notify_addr[2] == master_addr[2]:
-                        ret = self.xfrin_start(zone_name,
-                                               rrclass,
-                                               self._get_db_file(),
-                                               master_addr,
-                                               zone_info.get_tsig_key(), request_type,
-                                               True)
-                        answer = create_answer(ret[0], ret[1])
-                    else:
-                        notify_addr_str = format_addrinfo(notify_addr)
-                        master_addr_str = format_addrinfo(master_addr)
-                        errmsg = "Got notification for " + zone_str\
-                               + "from unknown address: " + notify_addr_str;
-                        logger.info(XFRIN_NOTIFY_UNKNOWN_MASTER, zone_str,
-                                    notify_addr_str, master_addr_str)
-                        answer = create_answer(1, errmsg)
-
-            elif command == 'retransfer' or command == 'refresh':
-                # Xfrin receives the retransfer/refresh from cmdctl(sent by bindctl).
-                # If the command has specified master address, do transfer from the
-                # master address, or else do transfer from the configured masters.
-                (zone_name, rrclass) = self._parse_zone_name_and_class(args)
-                master_addr = self._parse_master_and_port(args, zone_name,
-                                                          rrclass)
-                zone_info = self._get_zone_info(zone_name, rrclass)
-                tsig_key = None
-                request_type = RRType.AXFR
-                if zone_info:
-                    tsig_key = zone_info.get_tsig_key()
-                    if zone_info.use_ixfr:
-                        request_type = RRType.IXFR
-                db_file = args.get('db_file') or self._get_db_file()
-                ret = self.xfrin_start(zone_name,
-                                       rrclass,
-                                       db_file,
-                                       master_addr,
-                                       tsig_key, request_type,
-                                       (False if command == 'retransfer' else True))
-                answer = create_answer(ret[0], ret[1])
-
+                # refresh/notify command from zone manager.
+                # The address has to be validated, db_file is local only,
+                # and always perform SOA check.
+                addr_validator = \
+                    lambda x, y, z: self.__validate_notify_addr(x, y, z)
+                answer = self.__handle_xfr_command(args, None, True,
+                                                   addr_validator, None)
+            elif command == 'retransfer':
+                # retransfer from cmdctl (sent by bindctl).
+                # No need for address validation, db_file may be specified
+                # with the command, and skip SOA check, always use AXFR.
+                answer = self.__handle_xfr_command(
+                    args, args.get('db_file'), False, lambda x, y, z: None,
+                    ZoneInfo.REQUEST_IXFR_DISABLED)
+            elif command == 'refresh':
+                # retransfer from cmdctl (sent by bindctl).  similar to
+                # retransfer, but do SOA check, and honor request_ixfr config.
+                answer = self.__handle_xfr_command(
+                    args, args.get('db_file'), True, lambda x, y, z: None,
+                    None)
             # return statistics data to the stats daemon
             # return statistics data to the stats daemon
             elif command == "getstats":
             elif command == "getstats":
                 # The log level is here set to debug in order to avoid
                 # The log level is here set to debug in order to avoid
@@ -1608,7 +1729,8 @@ class Xfrin:
         if zone_name_str is None:
         if zone_name_str is None:
             raise XfrinException('zone name should be provided')
             raise XfrinException('zone name should be provided')
 
 
-        return (_check_zone_name(zone_name_str), _check_zone_class(args.get('zone_class')))
+        return (_check_zone_name(zone_name_str),
+                _check_zone_class(args.get('zone_class')))
 
 
     def _parse_master_and_port(self, args, zone_name, zone_class):
     def _parse_master_and_port(self, args, zone_name, zone_class):
         """
         """
@@ -1725,7 +1847,7 @@ class Xfrin:
             self._cc_check_command()
             self._cc_check_command()
 
 
     def xfrin_start(self, zone_name, rrclass, db_file, master_addrinfo,
     def xfrin_start(self, zone_name, rrclass, db_file, master_addrinfo,
-                    tsig_key, request_type, check_soa=True):
+                    tsig_key, request_ixfr, check_soa=True):
         if "pydnspp" not in sys.modules:
         if "pydnspp" not in sys.modules:
             return (1, "xfrin failed, can't load dns message python library: 'pydnspp'")
             return (1, "xfrin failed, can't load dns message python library: 'pydnspp'")
 
 
@@ -1744,7 +1866,7 @@ class Xfrin:
                                                 db_file,
                                                 db_file,
                                                 self._shutdown_event,
                                                 self._shutdown_event,
                                                 master_addrinfo, check_soa,
                                                 master_addrinfo, check_soa,
-                                                tsig_key, request_type))
+                                                tsig_key, request_ixfr))
 
 
         xfrin_thread.start()
         xfrin_thread.start()
         return (0, 'zone xfrin is started')
         return (0, 'zone xfrin is started')

+ 45 - 11
src/bin/xfrin/xfrin.spec

@@ -48,6 +48,11 @@
             "item_type": "boolean",
             "item_type": "boolean",
             "item_optional": false,
             "item_optional": false,
             "item_default": false
             "item_default": false
+          },
+          { "item_name": "request_ixfr",
+            "item_type": "string",
+            "item_optional": false,
+            "item_default": "yes"
           }
           }
           ]
           ]
         }
         }
@@ -56,7 +61,36 @@
     "commands": [
     "commands": [
      {
      {
         "command_name": "retransfer",
         "command_name": "retransfer",
-        "command_description": "retransfer a single zone without checking zone serial number",
+        "command_description": "retransfer a single zone without checking zone serial number, always using AXFR",
+        "command_args": [ {
+            "item_name": "zone_name",
+            "item_type": "string",
+            "item_optional": false,
+            "item_default": ""
+          },
+          {
+            "item_name": "zone_class",
+            "item_type": "string",
+            "item_optional": true,
+            "item_default": "IN"
+          },
+          {
+            "item_name": "master",
+            "item_type": "string",
+            "item_optional": true,
+            "item_default": ""
+          },
+          {
+            "item_name": "port",
+            "item_type": "integer",
+            "item_optional": true,
+            "item_default": 53
+          }
+        ]
+      },
+     {
+        "command_name": "refresh",
+        "command_description": "transfer a single zone with checking zone serial number and honoring the request_ixfr policy",
         "command_args": [ {
         "command_args": [ {
             "item_name": "zone_name",
             "item_name": "zone_name",
             "item_type": "string",
             "item_type": "string",
@@ -102,16 +136,16 @@
         "item_optional": false,
         "item_optional": false,
         "item_default": {
         "item_default": {
           "_SERVER_" : {
           "_SERVER_" : {
-	    "soaoutv4": 0,
-	    "soaoutv6": 0,
-	    "axfrreqv4": 0,
-	    "axfrreqv6": 0,
-	    "ixfrreqv4": 0,
-	    "ixfrreqv6": 0,
-	    "xfrsuccess": 0,
-	    "xfrfail": 0,
-	    "last_ixfr_duration": 0.0,
-	    "last_axfr_duration": 0.0
+            "soaoutv4": 0,
+            "soaoutv6": 0,
+            "axfrreqv4": 0,
+            "axfrreqv6": 0,
+            "ixfrreqv4": 0,
+            "ixfrreqv6": 0,
+            "xfrsuccess": 0,
+            "xfrfail": 0,
+            "last_ixfr_duration": 0.0,
+            "last_axfr_duration": 0.0
           }
           }
         },
         },
         "item_title": "Zone names",
         "item_title": "Zone names",

+ 32 - 3
src/bin/xfrin/xfrin_messages.mes

@@ -80,6 +80,24 @@ is not equal to the requested SOA serial.
 There was an error importing the python DNS module pydnspp. The most
 There was an error importing the python DNS module pydnspp. The most
 likely cause is a PYTHONPATH problem.
 likely cause is a PYTHONPATH problem.
 
 
+% XFRIN_INITIAL_AXFR no SOA available for %1 yet, requesting AXFR of initial version from %2
+On starting the zone transfer, it's detected that there is no SOA
+record available for the zone.  This is always the case for the very
+first transfer or if the administrator has removed the locally copied
+data by hand for some reason.  In this case trying IXFR does not make
+sense for the obvious reason, so AXFR will be used from the beginning,
+regardless of the request_ixfr configuration (even if "only" is
+specified).
+
+% XFRIN_INITIAL_IXFR requesting IXFR for %1 from %2
+IXFR will be used for the initial request type for the specified zone
+transfer.  It will fall back to AXFR if the initial request fails
+(and unless specified not to do so by configuration).
+
+% XFRIN_INITIAL_IXFR_DISABLED IXFR disabled for %1, requesting AXFR from %2
+The use of IXFR is disabled by configuration for the specified zone,
+so only AXFR will be tried.
+
 % XFRIN_INVALID_ZONE_DATA zone %1 received from %2 is broken and unusable
 % XFRIN_INVALID_ZONE_DATA zone %1 received from %2 is broken and unusable
 The zone was received, but it failed sanity validation. The previous version
 The zone was received, but it failed sanity validation. The previous version
 of zone (if any is available) will be used. Look for previous
 of zone (if any is available) will be used. Look for previous
@@ -212,6 +230,17 @@ such that the remote server doesn't support IXFR, we don't have the SOA record
 (or the zone at all), we are out of sync, etc. In many of these situations,
 (or the zone at all), we are out of sync, etc. In many of these situations,
 AXFR could still work. Therefore we try that one in case it helps.
 AXFR could still work. Therefore we try that one in case it helps.
 
 
+% XFRIN_XFR_TRANSFER_FALLBACK_DISABLED suppressing fallback from IXFR to AXFR for %1
+An IXFR transfer of the given zone failed.  By default AXFR will be
+tried next, but this fallback is disabled by configuration, so the
+whole transfer attempt failed at that point.  If the reason for the
+failure (which should be logged separately) is temporary, this is
+probably harmless or even desired as another IXFR will take place some
+time later (without falling back to the possibly expensive AXFR).  If
+this is a permanent error (e.g., some change at the master server
+completely disables IXFR), the secondary zone will eventually expire,
+so the configuration should be changed to allow AXFR.
+
 % XFRIN_XFR_TRANSFER_PROTOCOL_VIOLATION %1 transfer of zone %2 with %3 failed: %4
 % XFRIN_XFR_TRANSFER_PROTOCOL_VIOLATION %1 transfer of zone %2 with %3 failed: %4
 The XFR transfer for the given zone has failed due to a protocol
 The XFR transfer for the given zone has failed due to a protocol
 error, such as an unexpected response from the primary server.  The
 error, such as an unexpected response from the primary server.  The
@@ -250,9 +279,9 @@ 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
 transferred has multiple SOA RRs.  Such a zone is broken, but could be
 accidentally configured especially in a data source using "non
 accidentally configured especially in a data source using "non
 captive" backend database.  The implementation ignores entire SOA RRs
 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.
+and tries to continue processing as if the zone were empty.  This also
+means AXFR will be used unconditionally, regardless of the configured value
+for request_ixfr of the zone.
 
 
 % XFRIN_ZONE_NO_SOA Zone %1 does not have SOA
 % XFRIN_ZONE_NO_SOA Zone %1 does not have SOA
 On starting an xfrin session, it is identified that the zone to be
 On starting an xfrin session, it is identified that the zone to be

+ 27 - 1
src/lib/datasrc/client_list.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013  Internet Systems Consortium, Inc. ("ISC")
 //
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
 // purpose with or without fee is hereby granted, provided that the above
@@ -24,6 +24,7 @@
 #include <datasrc/memory/zone_data_loader.h>
 #include <datasrc/memory/zone_data_loader.h>
 #include <datasrc/memory/zone_data_updater.h>
 #include <datasrc/memory/zone_data_updater.h>
 #include <datasrc/logger.h>
 #include <datasrc/logger.h>
+#include <datasrc/zone_table_accessor_cache.h>
 #include <dns/masterload.h>
 #include <dns/masterload.h>
 #include <util/memory_segment_local.h>
 #include <util/memory_segment_local.h>
 
 
@@ -403,5 +404,30 @@ ConfigurableClientList::getStatus() const {
     return (result);
     return (result);
 }
 }
 
 
+ConstZoneTableAccessorPtr
+ConfigurableClientList::getZoneTableAccessor(const std::string& datasrc_name,
+                                             bool use_cache) const
+{
+    if (!use_cache) {
+        isc_throw(isc::NotImplemented,
+              "getZoneTableAccessor only implemented for cache");
+    }
+
+    // Find the matching data source
+    BOOST_FOREACH(const DataSourceInfo& info, data_sources_) {
+        if (!datasrc_name.empty() && datasrc_name != info.name_) {
+            continue;
+        }
+
+        const internal::CacheConfig* config(info.getCacheConfig());
+        // If caching is disabled for the named data source, this will
+        // return an accessor to an effectivley empty table.
+        return (ConstZoneTableAccessorPtr
+                (new internal::ZoneTableAccessorCache(*config)));
+    }
+
+    return (ConstZoneTableAccessorPtr());
+}
+
 }
 }
 }
 }

+ 35 - 3
src/lib/datasrc/client_list.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013  Internet Systems Consortium, Inc. ("ISC")
 //
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
 // purpose with or without fee is hereby granted, provided that the above
@@ -22,6 +22,7 @@
 #include <cc/data.h>
 #include <cc/data.h>
 #include <exceptions/exceptions.h>
 #include <exceptions/exceptions.h>
 #include "memory/zone_table_segment.h"
 #include "memory/zone_table_segment.h"
+#include <datasrc/zone_table_accessor.h>
 
 
 #include <vector>
 #include <vector>
 #include <boost/shared_ptr.hpp>
 #include <boost/shared_ptr.hpp>
@@ -120,6 +121,8 @@ private:
     MemorySegmentState state_;
     MemorySegmentState state_;
 };
 };
 
 
+typedef boost::shared_ptr<const ZoneTableAccessor> ConstZoneTableAccessorPtr;
+
 /// \brief The list of data source clients.
 /// \brief The list of data source clients.
 ///
 ///
 /// The purpose of this class is to hold several data source clients and search
 /// The purpose of this class is to hold several data source clients and search
@@ -281,6 +284,19 @@ public:
     virtual FindResult find(const dns::Name& zone,
     virtual FindResult find(const dns::Name& zone,
                             bool want_exact_match = false,
                             bool want_exact_match = false,
                             bool want_finder = true) const = 0;
                             bool want_finder = true) const = 0;
+
+    /// \brief Creates a ZoneTableAccessor object for the specified data
+    /// source.
+    ///
+    /// \param datasrc_name If not empty, the name of the data source.
+    /// \param use_cache If true, create a zone table for in-memory cache.
+    /// \throw NotImplemented if this method is not implemented.
+    /// \return A pointer to the accessor, or NULL if the requested data
+    /// source is not found.
+    virtual ConstZoneTableAccessorPtr
+    getZoneTableAccessor(const std::string& datasrc_name,
+                         bool use_cache) const = 0;
+
 };
 };
 
 
 /// \brief Shared pointer to the list.
 /// \brief Shared pointer to the list.
@@ -288,8 +304,8 @@ typedef boost::shared_ptr<ClientList> ClientListPtr;
 /// \brief Shared const pointer to the list.
 /// \brief Shared const pointer to the list.
 typedef boost::shared_ptr<const ClientList> ConstClientListPtr;
 typedef boost::shared_ptr<const ClientList> ConstClientListPtr;
 
 
-/// \Concrete implementation of the ClientList, which is constructed based on
-///     configuration.
+/// \brief Concrete implementation of the ClientList, which is constructed
+/// based on configuration.
 ///
 ///
 /// This is the implementation which is expected to be used in the servers.
 /// This is the implementation which is expected to be used in the servers.
 /// However, it is expected most of the code will use it as the ClientList,
 /// However, it is expected most of the code will use it as the ClientList,
@@ -490,6 +506,22 @@ public:
     /// it might be, so it is just made public (there's no real reason to
     /// it might be, so it is just made public (there's no real reason to
     /// hide it).
     /// hide it).
     const DataSources& getDataSources() const { return (data_sources_); }
     const DataSources& getDataSources() const { return (data_sources_); }
+
+    /// \brief Creates a ZoneTableAccessor object for the specified data
+    /// source.
+    ///
+    /// \param datasrc_name If not empty, the name of the data source
+    /// \param use_cache If true, create a zone table for in-memory cache.
+    /// \note At present, the only concrete implementation of
+    /// ZoneTableAccessor is ZoneTableAccessorCache, so \c use_cache must be
+    /// true.
+    /// \throw NotImplemented if \c use_cache is false.
+    /// \return A pointer to the accessor, or NULL if the requested data
+    /// source is not found.
+    ConstZoneTableAccessorPtr
+    getZoneTableAccessor(const std::string& datasrc_name,
+                         bool use_cache) const;
+
 private:
 private:
     struct MutableResult;
     struct MutableResult;
     /// \brief Internal implementation of find.
     /// \brief Internal implementation of find.

+ 90 - 1
src/lib/datasrc/tests/client_list_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013  Internet Systems Consortium, Inc. ("ISC")
 //
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
 // purpose with or without fee is hereby granted, provided that the above
@@ -266,6 +266,8 @@ public:
     }
     }
     ConfigurableClientList::CacheStatus doReload(
     ConfigurableClientList::CacheStatus doReload(
         const Name& origin, const string& datasrc_name = "");
         const Name& origin, const string& datasrc_name = "");
+    void accessorIterate(const ConstZoneTableAccessorPtr& accessor,
+        int numZones, const string& zoneName);
 
 
     const RRClass rrclass_;
     const RRClass rrclass_;
     shared_ptr<TestedList> list_;
     shared_ptr<TestedList> list_;
@@ -1147,6 +1149,93 @@ TEST_F(ListTest, reloadByDataSourceName) {
               doReload(Name("example.org"), "test_type4"));
               doReload(Name("example.org"), "test_type4"));
 }
 }
 
 
+// This takes the accessor provided by getZoneTableAccessor(), iterates
+// through the table, and verifies that the expected number of zones are
+// present, as well as the named zone.
+void
+ListTest::accessorIterate(const ConstZoneTableAccessorPtr& accessor,
+                          int numZones, const string& zoneName="")
+{
+    // Confirm basic iterator behavior.
+    ASSERT_TRUE(accessor);
+    ZoneTableAccessor::IteratorPtr it = accessor->getIterator();
+    ASSERT_TRUE(it);
+    // Iterator does not guarantee ordering, so we look for the target
+    // name anywhere in the table.
+    bool found = false;
+    int i;
+    for (i = 0; !it->isLast(); ++i, it->next()) {
+	if (Name(zoneName) == it->getCurrent().origin) {
+	    found = true;
+	}
+    }
+    EXPECT_EQ(i, numZones);
+    if (numZones > 0) {
+        EXPECT_TRUE(found);
+    }
+}
+
+TEST_F(ListTest, zoneTableAccessor) {
+    // empty configuration
+    const ConstElementPtr elem(new ListElement);
+    list_->configure(elem, true);
+    // null pointer treated as false
+    EXPECT_FALSE(list_->getZoneTableAccessor("", true));
+
+    // empty list; expect it to return an empty list
+    list_->configure(config_elem_, true);
+    ConstZoneTableAccessorPtr z(list_->getZoneTableAccessor("", true));
+    accessorIterate(z, 0);
+
+    const ConstElementPtr elem2(Element::fromJSON("["
+        "{"
+        "   \"type\": \"type1\","
+        "   \"cache-enable\": true,"
+        "   \"cache-zones\": [\"example.com\"],"
+        "   \"params\": [\"example.com\"]"
+        "},"
+        "{"
+        "   \"type\": \"type2\","
+        "   \"cache-enable\": false,"
+        "   \"params\": [\"example.org\"]"
+        "},"
+        "{"
+        "   \"type\": \"type3\","
+        "   \"cache-enable\": true,"
+        "   \"cache-zones\": [\"example.net\", \"example.info\"],"
+        "   \"params\": [\"example.net\", \"example.info\"]"
+        "}]"));
+
+    // allow_cache = false
+    // ask for a non-existent zone table, expect null
+    list_->configure(elem2, false);
+    EXPECT_FALSE(list_->getZoneTableAccessor("bogus", true));
+    // ask for any zone table, expect an empty list
+    z = list_->getZoneTableAccessor("", true);
+    accessorIterate(z, 0);
+
+    // allow_cache = true, use_cache = false
+    list_->configure(elem2, true);
+    EXPECT_THROW(list_->getZoneTableAccessor("", false), isc::NotImplemented);
+    EXPECT_THROW(list_->getZoneTableAccessor("type1", false),
+                 isc::NotImplemented);
+
+    // datasrc not found, returns NULL pointer
+    EXPECT_FALSE(list_->getZoneTableAccessor("bogus", true));
+
+    // return first datasrc
+    z = list_->getZoneTableAccessor("", true);
+    accessorIterate(z, 1, "example.com");
+
+    // datasrc has cache disabled, returns accessor to empty list
+    z = list_->getZoneTableAccessor("type2", true);
+    accessorIterate(z, 0);
+
+    // search by name
+    z = list_->getZoneTableAccessor("type3", true);
+    accessorIterate(z, 2, "example.net");
+}
+
 // Check the status holds data
 // Check the status holds data
 TEST(DataSourceStatus, status) {
 TEST(DataSourceStatus, status) {
     const DataSourceStatus status("Test", SEGMENT_INUSE, "local");
     const DataSourceStatus status("Test", SEGMENT_INUSE, "local");

+ 0 - 1
tests/lettuce/configurations/ixfr-out/testset1-config.db

@@ -2,7 +2,6 @@
     "Xfrin": {
     "Xfrin": {
         "zones": [
         "zones": [
             {
             {
-                "use_ixfr": true,
                 "class": "IN",
                 "class": "IN",
                 "name": "example.com.",
                 "name": "example.com.",
                 "master_addr": "178.18.82.80"
                 "master_addr": "178.18.82.80"

+ 1 - 2
tests/lettuce/configurations/xfrin/retransfer_slave_diffs.conf

@@ -18,8 +18,7 @@
         "zones": [ {
         "zones": [ {
             "name": "example",
             "name": "example",
             "master_addr": "::1",
             "master_addr": "::1",
-            "master_port": 47807,
-            "use_ixfr": true
+            "master_port": 47807
         } ]
         } ]
     },
     },
     "data_sources": {
     "data_sources": {

+ 2 - 1
tests/lettuce/configurations/xfrin/retransfer_slave_notify.conf.orig

@@ -28,7 +28,8 @@
         "zones": [ {
         "zones": [ {
             "name": "example.org",
             "name": "example.org",
             "master_addr": "::1",
             "master_addr": "::1",
-            "master_port": 47807
+            "master_port": 47807,
+            "request_ixfr": "no"
         } ]
         } ]
     },
     },
     "Zonemgr": {
     "Zonemgr": {

+ 2 - 1
tests/lettuce/configurations/xfrin/retransfer_slave_notify_v4.conf

@@ -28,7 +28,8 @@
         "zones": [ {
         "zones": [ {
             "name": "example.org",
             "name": "example.org",
             "master_addr": "127.0.0.1",
             "master_addr": "127.0.0.1",
-            "master_port": 47809
+            "master_port": 47809,
+            "request_ixfr": "no"
         } ]
         } ]
     },
     },
     "Zonemgr": {
     "Zonemgr": {

+ 2 - 1
tests/lettuce/features/xfrin_bind10.feature

@@ -182,7 +182,8 @@ Feature: Xfrin
     example.    3600    IN      SOA     ns1.example. hostmaster.example. 94 3600 900 7200 300
     example.    3600    IN      SOA     ns1.example. hostmaster.example. 94 3600 900 7200 300
     """
     """
 
 
-    When I send bind10 the command Xfrin retransfer example. IN ::1 47807
+    # To invoke IXFR we need to use refresh command
+    When I send bind10 the command Xfrin refresh example. IN ::1 47807
     Then wait for new bind10 stderr message XFRIN_GOT_INCREMENTAL_RESP
     Then wait for new bind10 stderr message XFRIN_GOT_INCREMENTAL_RESP
     Then wait for new bind10 stderr message XFRIN_IXFR_TRANSFER_SUCCESS not XFRIN_XFR_PROCESS_FAILURE
     Then wait for new bind10 stderr message XFRIN_IXFR_TRANSFER_SUCCESS not XFRIN_XFR_PROCESS_FAILURE
     # This can't be 'wait for new'
     # This can't be 'wait for new'