BIND 10 master, updated. 8191aec04c5279c199909f00f0a0b2b8f7bede94 [master] Merge branch 'trac2964'

BIND 10 source code commits bind10-changes at lists.isc.org
Thu Jun 6 19:27:52 UTC 2013


The branch, master has been updated
       via  8191aec04c5279c199909f00f0a0b2b8f7bede94 (commit)
       via  09e6babe2643d0222b4ff99517834cdb07d80aae (commit)
       via  b02f05774d65cac0b87452dfea6a65be73e08069 (commit)
       via  1ce2bb2f000d2a11a2e1e299e62ee567d81c8684 (commit)
       via  3bd3919f267d320eec044a8fb8d2aed62f176e08 (commit)
       via  b33bcfe2c8c1b651f5da66e03ea7fdbf1f499c36 (commit)
       via  c269cdc11c02affd7b191a4aabcc56bf6894ef3a (commit)
       via  24d49c3c491cf82fb5e0134d28e8eecc2428851e (commit)
       via  547094b71f9306e1130926ccb76160141e2b080e (commit)
       via  9b95e643da6c3b1be36b0700e8198b9fec870515 (commit)
       via  53d462aedda7e6f2527dff34a6da25ef58ec298b (commit)
       via  1867eb5f882da2cc6cb5c8be897d9affcf125e28 (commit)
       via  f24dff697d824255a76586d0ff879e8445087d08 (commit)
       via  429d200e948f44dd4d7f668c2f8d46883f576205 (commit)
       via  4ca40253f70421e0133e2bcc18dde133b4851f67 (commit)
       via  525021e0a8f3c00757b66588208bc10528544416 (commit)
       via  e51d6fd38a9ecdb6d8ff200fc152aaf5bf1e0cf5 (commit)
       via  edc1911c2dd4b941d4658aa0e677f4a8ef4ce819 (commit)
       via  1c5051938f58da3dda1b327d373b4df7f09ca7de (commit)
       via  7a0ffbd7818d721d06b1864fe840f89be6f65b8b (commit)
       via  43c9101c5e4a721fde8f6699dde7f45f12b80823 (commit)
       via  159d0dd5e07877315eaa9690cfe141f3dac1f1c6 (commit)
       via  42213653b829ee68eae11af916dbd2164c96d27f (commit)
       via  2ed196723ce382b98667029ae24dae42361197df (commit)
       via  8b143c88e7413b43086ca538e84af945f7754447 (commit)
      from  f49e54056a5485a18ab7181256233c3af7f1a73f (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 8191aec04c5279c199909f00f0a0b2b8f7bede94
Merge: f49e540 09e6bab
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Thu Jun 6 12:18:04 2013 -0700

    [master] Merge branch 'trac2964'

-----------------------------------------------------------------------

Summary of changes:
 doc/guide/bind10-guide.xml                         |   34 +-
 src/bin/loadzone/b10-loadzone.xml                  |   33 ++
 src/bin/loadzone/loadzone.py.in                    |   64 +++-
 src/bin/loadzone/loadzone_messages.mes             |    5 +
 src/bin/loadzone/tests/loadzone_test.py            |   37 ++-
 src/bin/xfrin/tests/xfrin_test.py                  |  303 +++++++-----------
 src/bin/xfrin/xfrin.py.in                          |  325 ++++++++------------
 src/bin/xfrin/xfrin_messages.mes                   |   34 +-
 src/lib/python/isc/server_common/Makefile.am       |    1 +
 .../isc/server_common/datasrc_clients_mgr.py       |  135 ++++++++
 src/lib/python/isc/server_common/tests/Makefile.am |   11 +-
 .../tests/datasrc_clients_mgr_test.py              |  110 +++++++
 tests/lettuce/features/terrain/loadzone.py         |   24 +-
 tests/lettuce/features/xfrin_bind10.feature        |   16 +-
 14 files changed, 686 insertions(+), 446 deletions(-)
 create mode 100644 src/lib/python/isc/server_common/datasrc_clients_mgr.py
 create mode 100644 src/lib/python/isc/server_common/tests/datasrc_clients_mgr_test.py

-----------------------------------------------------------------------
diff --git a/doc/guide/bind10-guide.xml b/doc/guide/bind10-guide.xml
index 9d1986d..88f2536 100644
--- a/doc/guide/bind10-guide.xml
+++ b/doc/guide/bind10-guide.xml
@@ -2624,7 +2624,7 @@ can use various data source backends.
         There's also <varname>Auth/database_file</varname> configuration
         variable, pointing to a SQLite3 database file. This is no longer
         used by <command>b10-auth</command>, but it is left in place for
-        now, since other modules use it. Once <command>b10-xfrin</command>,
+        now, since other modules use it. Once <command>b10-zonemgr</command>,
         <command>b10-xfrout</command> and <command>b10-ddns</command>
         are ported to the new configuration, this will disappear. But for
         now, make sure that if you use any of these modules, the new
@@ -2731,10 +2731,23 @@ TODO
     <section>
       <title>Configuration for Incoming Zone Transfers</title>
       <para>
-        In practice, you need to specify a list of secondary zones to
-        enable incoming zone transfers for these zones (you can still
-        trigger a zone transfer manually, without a prior configuration
-        (see below)).
+	In order to enable incoming zone transfers for a secondary
+	zone, you will first need to make the zone "exist" in some
+	data source.
+	One easy way to do this is to create an empty zone using the
+	<command>b10-loadzone</command> utility.
+	For example, this makes an empty zone (or empties any existing
+	content of the zone) "example.com" in the default data source
+	for <command>b10-loadzone</command> (which is SQLite3-based
+	data source):
+            <screen>$ <userinput>b10-loadzone <replaceable>-e</replaceable> <replaceable>example.com</replaceable></userinput></screen>
+      </para>
+
+      <para>
+        Next, you need to specify a list of secondary zones to
+        enable incoming zone transfers for these zones in most
+        practical cases (you can still trigger a zone transfer
+        manually, without a prior configuration (see below)).
       </para>
 
       <para>
@@ -2749,6 +2762,17 @@ TODO
 
       (We assume there has been no zone configuration before).
       </para>
+
+      <note>
+        <simpara>
+	  There is a plan to revise overall zone management
+	  configuration (which are primary and secondary zones, which
+	  data source they are stored, etc) so it can be configured
+	  more consistently and in a unified way among various BIND 10 modules.
+	  When it's done, part or all of the initial configuration
+	  setup described in this section may be deprecated.
+	</simpara>
+      </note>
     </section>
 
     <section>
diff --git a/src/bin/loadzone/b10-loadzone.xml b/src/bin/loadzone/b10-loadzone.xml
index aa14053..b47421f 100644
--- a/src/bin/loadzone/b10-loadzone.xml
+++ b/src/bin/loadzone/b10-loadzone.xml
@@ -52,6 +52,12 @@
       <arg choice="req">zone name</arg>
       <arg choice="req">zone file</arg>
     </cmdsynopsis>
+    <cmdsynopsis>
+      <command>b10-loadzone</command>
+      <arg><option>-e</option></arg>
+      <arg><option>other options</option></arg>
+      <arg choice="req">zone name</arg>
+    </cmdsynopsis>
   </refsynopsisdiv>
 
   <refsect1>
@@ -61,6 +67,9 @@
       in a BIND 10 ready data source format.
       Master files are text files that contain DNS Resource Records
       in text form.
+      This utility can also be used to create an empty zone on the
+      specified data source so the existence of the zone is recognized
+      in the data source without any content (resource records).
     </para>
     <note><simpara>Currently only the SQLITE3 data source is supported.
     </simpara></note>
@@ -104,6 +113,18 @@
       old version will still remain accessible for other applications.
     </para>
 
+    <para>
+      If the <command>-e</command> command line option is specified,
+      <command>b10-loadzone</command> does not take the zone name
+      argument.
+      In this case it creates an empty zone without any content
+      while the data source still recognizes the existence of the
+      zone.
+      If the specified zone already has some content, this mode of
+      operation will remove it (but the existence of the zone in the
+      data source will be still recognized).
+    </para>
+
   </refsect1>
 
   <refsect1>
@@ -142,6 +163,18 @@
       </varlistentry>
 
       <varlistentry>
+        <term>-e</term>
+        <listitem><para>
+	    Create an empty zone, or empty existing zone content
+	    instead of loading new one.
+	    When this option is specified, the zone file command line
+	    argument must not be provided.
+	    The <command>-i</command> option has no effect, but it
+	    does not cause a failure; it will be simply ignored.
+        </para></listitem>
+      </varlistentry>
+
+      <varlistentry>
         <term>-i <replaceable class="parameter">report_interval</replaceable></term>
         <listitem><para>
           Specifies the interval of status update by the number of RRs
diff --git a/src/bin/loadzone/loadzone.py.in b/src/bin/loadzone/loadzone.py.in
index 736aa31..1203e45 100755
--- a/src/bin/loadzone/loadzone.py.in
+++ b/src/bin/loadzone/loadzone.py.in
@@ -66,6 +66,8 @@ Example: '{"database_file": "/path/to/dbfile/db.sqlite3"}'""",
     parser.add_option("-d", "--debug", dest="debug_level",
                       type='int', action="store", default=None,
                       help="enable debug logs with the specified level [0-99]")
+    parser.add_option("-e", "--empty", dest="empty_zone",
+                      action="store_true", help="empty zone content (no load)")
     parser.add_option("-i", "--report-interval", dest="report_interval",
                       type='int', action="store",
                       default=LOAD_INTERVAL_DEFAULT,
@@ -113,6 +115,7 @@ class LoadZoneRunner:
         self._datasrc_type = None
         self._log_severity = 'INFO'
         self._log_debuglevel = 0
+        self._empty_zone = False
         self._report_interval = LOAD_INTERVAL_DEFAULT
         self._start_time = None
         # This one will be used in (rare) cases where we want to allow tests to
@@ -140,7 +143,8 @@ class LoadZoneRunner:
         '''
 
         usage_txt = \
-            'usage: %prog [options] -c datasrc_config zonename zonefile'
+            'usage: %prog [options] -c datasrc_config zonename zonefile\n' + \
+            '       %prog [options] -c datasrc_config -e zonename'
         parser = OptionParser(usage=usage_txt)
         set_cmd_options(parser)
         (options, args) = parser.parse_args(args=self.__command_args)
@@ -174,15 +178,22 @@ class LoadZoneRunner:
                 'Invalid report interval (must be non negative): %d' %
                 self._report_interval)
 
-        if len(args) != 2:
-            raise BadArgument('Unexpected number of arguments: %d (must be 2)'
-                              % (len(args)))
+        if options.empty_zone:
+            self._empty_zone = True
+
+        # Check number of non option arguments: must be 1 with -e; 2 otherwise.
+        num_args = 1 if self._empty_zone else 2
+
+        if len(args) != num_args:
+            raise BadArgument('Unexpected number of arguments: %d (must be %d)'
+                              % (len(args), num_args))
         try:
             self._zone_name = Name(args[0])
         except Exception as ex: # too broad, but there's no better granurality
             raise BadArgument("Invalid zone name '" + args[0] + "': " +
                               str(ex))
-        self._zone_file = args[1]
+        if len(args) > 1:
+            self._zone_file = args[1]
 
     def _get_datasrc_config(self, datasrc_type):
         ''''Return the default data source configuration of given type.
@@ -254,6 +265,34 @@ class LoadZoneRunner:
             else:
                 logger.info(LOADZONE_ZONE_UPDATING, self._zone_name,
                             self._zone_class)
+            if self._empty_zone:
+                self.__make_empty_zone(datasrc_client)
+            else:
+                self.__load_from_file(datasrc_client)
+        except Exception as ex:
+            if created:
+                datasrc_client.delete_zone(self._zone_name)
+                logger.error(LOADZONE_CANCEL_CREATE_ZONE, self._zone_name,
+                             self._zone_class)
+            raise LoadFailure(str(ex))
+
+    def __make_empty_zone(self, datasrc_client):
+        """Subroutine of _do_load(), create an empty zone or make it empty."""
+        try:
+            updater = datasrc_client.get_updater(self._zone_name, True)
+            updater.commit()
+            logger.info(LOADZONE_EMPTY_DONE, self._zone_name,
+                        self._zone_class)
+        except Exception:
+            # once updater is created, it's very unlikely that commit() fails,
+            # but in case it happens, clear updater to release any remaining
+            # lock.
+            updater = None
+            raise
+
+    def __load_from_file(self, datasrc_client):
+        """Subroutine of _do_load(), load a zone file into data source."""
+        try:
             loader = ZoneLoader(datasrc_client, self._zone_name,
                                 self._zone_file)
             self._start_time = time.time()
@@ -279,14 +318,14 @@ class LoadZoneRunner:
                 sys.stdout.write('\n')
             # record the final count of the loaded RRs for logging
             self._loaded_rrs = loader.get_rr_count()
-        except Exception as ex:
+
+            total_elapsed_txt = "%.2f" % (time.time() - self._start_time)
+            logger.info(LOADZONE_DONE, self._loaded_rrs, self._zone_name,
+                        self._zone_class, total_elapsed_txt)
+        except Exception:
             # release any remaining lock held in the loader
             loader = None
-            if created:
-                datasrc_client.delete_zone(self._zone_name)
-                logger.error(LOADZONE_CANCEL_CREATE_ZONE, self._zone_name,
-                             self._zone_class)
-            raise LoadFailure(str(ex))
+            raise
 
     def _set_signal_handlers(self):
         signal.signal(signal.SIGINT, self._interrupt_handler)
@@ -302,9 +341,6 @@ class LoadZoneRunner:
             self._set_signal_handlers()
             self._parse_args()
             self._do_load()
-            total_elapsed_txt = "%.2f" % (time.time() - self._start_time)
-            logger.info(LOADZONE_DONE, self._loaded_rrs, self._zone_name,
-                        self._zone_class, total_elapsed_txt)
             return 0
         except BadArgument as ex:
             logger.error(LOADZONE_ARGUMENT_ERROR, ex)
diff --git a/src/bin/loadzone/loadzone_messages.mes b/src/bin/loadzone/loadzone_messages.mes
index 744a1a4..206b02e 100644
--- a/src/bin/loadzone/loadzone_messages.mes
+++ b/src/bin/loadzone/loadzone_messages.mes
@@ -33,6 +33,11 @@ an old version of the zone in the data source, it is now deleted.
 It also prints the number of RRs that have been loaded
 and the time spent for the loading.
 
+% LOADZONE_EMPTY_DONE Completed emptying zone %1/%2
+b10-loadzone has successfully emptied content of the specified zone.
+This includes the case where the content didn't previously exist, in which
+case it just still reamins empty.
+
 % LOADZONE_LOAD_ERROR Failed to load zone %1/%2: %3
 Loading a zone by b10-loadzone fails for some reason in the middle of
 the loading.  This is most likely due to an error in the specified
diff --git a/src/bin/loadzone/tests/loadzone_test.py b/src/bin/loadzone/tests/loadzone_test.py
index 16499ba..83894bd 100755
--- a/src/bin/loadzone/tests/loadzone_test.py
+++ b/src/bin/loadzone/tests/loadzone_test.py
@@ -82,6 +82,7 @@ class TestLoadZoneRunner(unittest.TestCase):
         self.assertEqual(RRClass.IN, self.__runner._zone_class) # default
         self.assertEqual('INFO', self.__runner._log_severity) # default
         self.assertEqual(0, self.__runner._log_debuglevel)
+        self.assertFalse(self.__runner._empty_zone)
 
     def test_set_loglevel(self):
         runner = LoadZoneRunner(['-d', '1'] + self.__args)
@@ -90,13 +91,19 @@ class TestLoadZoneRunner(unittest.TestCase):
         self.assertEqual(1, runner._log_debuglevel)
 
     def test_parse_bad_args(self):
-        # There must be exactly 2 non-option arguments: zone name and zone file
+        # There must usually be exactly 2 non-option arguments: zone name and
+        # zone file.
         self.assertRaises(BadArgument, LoadZoneRunner([])._parse_args)
         self.assertRaises(BadArgument, LoadZoneRunner(['example']).
                           _parse_args)
         self.assertRaises(BadArgument, LoadZoneRunner(self.__args + ['0']).
                           _parse_args)
 
+        # With -e it must be only zone name
+        self.assertRaises(BadArgument, LoadZoneRunner(
+                ['-e', 'example', 'example.zone'])._parse_args)
+        self.assertRaises(BadArgument, LoadZoneRunner(['-e'])._parse_args)
+
         # Bad zone name
         args = ['example.org', 'example.zone'] # otherwise valid args
         self.assertRaises(BadArgument,
@@ -134,22 +141,24 @@ class TestLoadZoneRunner(unittest.TestCase):
         self.assertRaises(BadArgument, self.__runner._get_datasrc_config,
                           'memory')
 
-    def __common_load_setup(self):
+    def __common_load_setup(self, empty=False):
         self.__runner._zone_class = RRClass.IN
         self.__runner._zone_name = TEST_ZONE_NAME
         self.__runner._zone_file = NEW_ZONE_TXT_FILE
         self.__runner._datasrc_type = 'sqlite3'
         self.__runner._datasrc_config = DATASRC_CONFIG
         self.__runner._report_interval = 1
+        self.__runner._empty_zone = empty
         self.__reports = []
         self.__runner._report_progress = lambda x, _: self.__reports.append(x)
 
     def __check_zone_soa(self, soa_txt, zone_name=TEST_ZONE_NAME):
         """Check that the given SOA RR exists and matches the expected string
 
-        If soa_txt is None, the zone is expected to be non-existent.
-        Otherwise, if soa_txt is False, the zone should exist but SOA is
-        expected to be missing.
+        If soa_txt is None, the zone is expected to be non-existent;
+        if it's 'empty', the zone should exist but is expected to be empty;
+        if soa_txt is False, the zone should exist but SOA is expected to be
+        missing.
 
         """
 
@@ -160,7 +169,10 @@ class TestLoadZoneRunner(unittest.TestCase):
             return
         self.assertEqual(client.SUCCESS, result)
         result, rrset, _ = finder.find(zone_name, RRType.SOA)
-        if soa_txt:
+        if soa_txt == 'empty':
+            self.assertEqual(finder.NXDOMAIN, result)
+            self.assertIsNone(rrset)
+        elif soa_txt:
             self.assertEqual(finder.SUCCESS, result)
             self.assertEqual(soa_txt, rrset.to_text())
         else:
@@ -269,6 +281,19 @@ class TestLoadZoneRunner(unittest.TestCase):
         # _do_load() should have once created the zone but then canceled it.
         self.__check_zone_soa(None, zone_name=Name('example.com'))
 
+    def test_create_and_empty(self):
+        self.__common_load_setup(True)
+        self.__runner._zone_name = Name('example.com')
+        self.__check_zone_soa(None, zone_name=Name('example.com'))
+        self.__runner._do_load()
+        self.__check_zone_soa('empty', zone_name=Name('example.com'))
+
+    def test_empty(self):
+        self.__common_load_setup(True)
+        self.__check_zone_soa(ORIG_SOA_TXT)
+        self.__runner._do_load()
+        self.__check_zone_soa('empty')
+
     def __common_post_load_setup(self, zone_file):
         '''Common setup procedure for post load tests which should fail.'''
         # replace the LoadZoneRunner's original _post_load_warning() for
diff --git a/src/bin/xfrin/tests/xfrin_test.py b/src/bin/xfrin/tests/xfrin_test.py
index ae2d14b..bfcbdcb 100644
--- a/src/bin/xfrin/tests/xfrin_test.py
+++ b/src/bin/xfrin/tests/xfrin_test.py
@@ -135,9 +135,11 @@ class MockCC(MockModuleCCSession, ConfigData):
         module_spec = isc.config.module_spec_from_file(
             xfrin.SPECFILE_LOCATION)
         ConfigData.__init__(self, module_spec)
+        # For inspection
+        self.added_remote_modules = []
 
     def add_remote_config_by_name(self, name, callback):
-        pass
+        self.added_remote_modules.append((name, callback))
 
     def get_remote_config_value(self, module, identifier):
         if module == 'tsig_keys' and identifier == 'keys':
@@ -241,6 +243,33 @@ class MockDataSourceClient():
         # pretend it just succeeds
         pass
 
+class MockDataSrcClientsMgr():
+    def __init__(self):
+        # Default faked result of get_client_list, customizable by tests
+        self.found_datasrc_client_list = self
+
+        # Default faked result of find(), customizable by tests
+        self.found_datasrc_client = MockDataSourceClient()
+
+        self.reconfigure_param = [] # for inspection
+
+    def get_client_list(self, rrclass):
+        return self.found_datasrc_client_list
+
+    def reconfigure(self, arg1):
+        # the only current test simply needs to know this is called with
+        # the expected argument and exceptions are handled.  if we need more
+        # variations in tests, this mock method should be extended.
+        self.reconfigure_param.append(arg1)
+        raise isc.server_common.datasrc_clients_mgr.ConfigError(
+            'reconfigure failure')
+
+    def find(self, zone_name, want_exact_match, want_finder):
+        """Pretending find method on the object returned by get_clinet_list"""
+        if issubclass(type(self.found_datasrc_client), Exception):
+            raise self.found_datasrc_client
+        return self.found_datasrc_client, None, None
+
 class MockXfrin(Xfrin):
     # This is a class attribute of a callable object that specifies a non
     # default behavior triggered in _cc_check_command().  Specific test methods
@@ -250,29 +279,22 @@ class MockXfrin(Xfrin):
     check_command_hook = None
 
     def _cc_setup(self):
-        self._tsig_key = None
         self._module_cc = MockCC()
-        init_keyring(self._module_cc)
-        pass
-
-    def _get_db_file(self):
-        pass
 
     def _cc_check_command(self):
         self._shutdown_event.set()
         if 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, master_addrinfo,
                     tsig_key, request_ixfr, check_soa=True):
         # store some of the arguments for verification, then call this
         # method in the superclass
         self.xfrin_started_master_addr = master_addrinfo[2][0]
         self.xfrin_started_master_port = master_addrinfo[2][1]
         self.xfrin_started_request_ixfr = request_ixfr
-        return Xfrin.xfrin_start(self, zone_name, rrclass, None,
-                                 master_addrinfo, tsig_key,
-                                 request_ixfr, check_soa)
+        return Xfrin.xfrin_start(self, zone_name, rrclass, master_addrinfo,
+                                 tsig_key, request_ixfr, check_soa)
 
 class MockXfrinConnection(XfrinConnection):
     def __init__(self, sock_map, zone_name, rrclass, datasrc_client,
@@ -845,9 +867,11 @@ class TestXfrinConnection(unittest.TestCase):
 
         '''
         self.conn._zone_name = zone_name
-        self.conn._zone_soa = xfrin._get_zone_soa(self.conn._datasrc_client,
-                                                  zone_name,
-                                                  self.conn._rrclass)
+        try:
+            self.conn._zone_soa = xfrin._get_zone_soa(
+                self.conn._datasrc_client, zone_name, self.conn._rrclass)
+        except XfrinException:  # zone doesn't exist
+            self.conn._zone_soa = None
 
 class TestAXFR(TestXfrinConnection):
     def setUp(self):
@@ -981,7 +1005,7 @@ class TestAXFR(TestXfrinConnection):
         def message_has_tsig(data):
             # a simple check if the actual data contains a TSIG RR.
             # At our level this simple check should suffice; other detailed
-            # tests regarding the TSIG protocol are done in pydnspp.
+            # tests regarding the TSIG protocol are done in the isc.dns module.
             msg = Message(Message.PARSE)
             msg.from_wire(data)
             return msg.get_tsig_record() is not None
@@ -2100,32 +2124,6 @@ class TestXFRSessionWithSQLite3(TestXfrinConnection):
         '''
         self.axfr_failure_check(RRType.AXFR)
 
-    def test_do_axfrin_nozone_sqlite3(self):
-        '''AXFR test with an empty SQLite3 DB file, thus no target zone there.
-
-        For now, we provide backward compatible behavior: xfrin will create
-        the zone (after even setting up the entire schema) in the zone.
-        Note: a future version of this test will make it fail.
-
-        '''
-        self.conn._db_file = self.empty_sqlite3db_obj
-        self.conn._datasrc_client = DataSourceClient(
-            "sqlite3",
-            "{ \"database_file\": \"" + self.empty_sqlite3db_obj + "\"}")
-        def create_response():
-            self.conn.reply_data = self.conn.create_response_data(
-                questions=[Question(TEST_ZONE_NAME, TEST_RRCLASS,
-                                    RRType.AXFR)],
-                answers=[soa_rrset, self._create_ns(), soa_rrset])
-        self.conn.response_generator = create_response
-        self._set_test_zone(Name('example.com'))
-        self.assertEqual(XFRIN_OK, self.conn.do_xfrin(False, RRType.AXFR))
-        self.assertEqual(type(XfrinAXFREnd()),
-                         type(self.conn.get_xfrstate()))
-        self.assertEqual(1234, self.get_zone_serial().get_value())
-        self.assertFalse(self.record_exist(Name('dns01.example.com'),
-                                           RRType.A))
-
 class TestStatisticsXfrinConn(TestXfrinConnection):
     '''Test class based on TestXfrinConnection and including paramters
     and methods related to statistics tests'''
@@ -2414,16 +2412,16 @@ class TestXfrinProcess(unittest.TestCase):
         # Normal, successful case.  We only check that things are cleaned up
         # at the tearDown time.
         process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
-                      self.master,  False, None, ZoneInfo.REQUEST_IXFR_DISABLED,
-                      self.create_xfrinconn)
+                      None, self.master,  False, None,
+                      ZoneInfo.REQUEST_IXFR_DISABLED, self.create_xfrinconn)
 
     def test_process_xfrin_exception_on_connect(self):
         # connect_to_master() will raise an exception.  Things must still be
         # cleaned up.
         self.do_raise_on_connect = True
         process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
-                      self.master,  False, None, ZoneInfo.REQUEST_IXFR_DISABLED,
-                      self.create_xfrinconn)
+                      None, self.master,  False, None,
+                      ZoneInfo.REQUEST_IXFR_DISABLED, self.create_xfrinconn)
 
     def test_process_xfrin_exception_on_close(self):
         # connect() will result in exception, and even the cleanup close()
@@ -2432,38 +2430,48 @@ class TestXfrinProcess(unittest.TestCase):
         self.do_raise_on_connect = True
         self.do_raise_on_close = True
         process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
-                      self.master,  False, None, ZoneInfo.REQUEST_IXFR_DISABLED,
-                      self.create_xfrinconn)
+                      None, self.master,  False, None,
+                      ZoneInfo.REQUEST_IXFR_DISABLED, self.create_xfrinconn)
 
     def test_process_xfrin_exception_on_publish(self):
         # xfr succeeds but notifying the zonemgr fails with exception.
         # everything must still be cleaned up.
         self.do_raise_on_publish = True
         process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
-                      self.master,  False, None, ZoneInfo.REQUEST_IXFR_DISABLED,
-                      self.create_xfrinconn)
+                      None, self.master,  False, None,
+                      ZoneInfo.REQUEST_IXFR_DISABLED, self.create_xfrinconn)
 
 class TestXfrin(unittest.TestCase):
     def setUp(self):
         # redirect output
         self.stderr_backup = sys.stderr
         sys.stderr = open(os.devnull, 'w')
+        self.__orig_DataSrcClientsMgr = xfrin.DataSrcClientsMgr
+        xfrin.DataSrcClientsMgr = MockDataSrcClientsMgr
+
         self.xfr = MockXfrin()
         self.args = {}
         self.args['zone_name'] = TEST_ZONE_NAME_STR
         self.args['class'] = TEST_RRCLASS_STR
         self.args['port'] = TEST_MASTER_PORT
         self.args['master'] = TEST_MASTER_IPV4_ADDRESS
-        self.args['db_file'] = TEST_DB_FILE
         self.args['tsig_key'] = ''
 
     def tearDown(self):
+        xfrin.DataSrcClientsMgr = self.__orig_DataSrcClientsMgr
         self.assertFalse(self.xfr._module_cc.stopped);
         self.xfr.shutdown()
         self.assertTrue(self.xfr._module_cc.stopped);
         sys.stderr.close()
         sys.stderr = self.stderr_backup
 
+    def test_init(self):
+        """Check some initial configuration after construction"""
+        # data source "module" should have been registrered as a necessary
+        # remote config
+        self.assertEqual([('data_sources', self.xfr._datasrc_config_handler)],
+                         self.xfr._module_cc.added_remote_modules)
+
     def _do_parse_zone_name_class(self):
         return self.xfr._parse_zone_name_and_class(self.args)
 
@@ -2474,12 +2482,10 @@ class TestXfrin(unittest.TestCase):
     def test_parse_cmd_params(self):
         name, rrclass = self._do_parse_zone_name_class()
         master_addrinfo = self._do_parse_master_port()
-        db_file = self.args.get('db_file')
         self.assertEqual(master_addrinfo[2][1], int(TEST_MASTER_PORT))
         self.assertEqual(name, TEST_ZONE_NAME)
         self.assertEqual(rrclass, TEST_RRCLASS)
         self.assertEqual(master_addrinfo[2][0], TEST_MASTER_IPV4_ADDRESS)
-        self.assertEqual(db_file, TEST_DB_FILE)
 
     def test_parse_cmd_params_default_port(self):
         del self.args['port']
@@ -2537,9 +2543,11 @@ class TestXfrin(unittest.TestCase):
     def test_command_handler_retransfer(self):
         self.assertEqual(self.xfr.command_handler("retransfer",
                                                   self.args)['result'][0], 0)
-        self.assertEqual(self.args['master'], self.xfr.xfrin_started_master_addr)
-        self.assertEqual(int(self.args['port']), self.xfr.xfrin_started_master_port)
-        # By default we use AXFR (for now)
+        self.assertEqual(self.args['master'],
+                         self.xfr.xfrin_started_master_addr)
+        self.assertEqual(int(self.args['port']),
+                         self.xfr.xfrin_started_master_port)
+        # retransfer always uses AXFR
         self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
                          self.xfr.xfrin_started_request_ixfr)
 
@@ -2636,13 +2644,36 @@ class TestXfrin(unittest.TestCase):
         self.assertEqual(self.xfr.command_handler("retransfer",
                                                   self.args)['result'][0], 1)
 
-    def test_command_handler_retransfer_nomodule(self):
-        dns_module = sys.modules['pydnspp'] # this must exist
-        del sys.modules['pydnspp']
-        self.assertEqual(self.xfr.command_handler("retransfer",
-                                                  self.args)['result'][0], 1)
-        # sys.modules is global, so we must recover it
-        sys.modules['pydnspp'] = dns_module
+    def test_command_handler_retransfer_datasrc_error(self):
+        # Failure cases due to various errors at the data source (config/data)
+        # level
+
+        # No data source client list for the RR class
+        self.xfr._datasrc_clients_mgr.found_datasrc_client_list = None
+        self.assertEqual(1, self.xfr.command_handler("retransfer",
+                                                     self.args)['result'][0])
+
+        # No  data source client for the zone name
+        self.xfr._datasrc_clients_mgr.found_datasrc_client_list = \
+            self.xfr._datasrc_clients_mgr # restore the original
+        self.xfr._datasrc_clients_mgr.found_datasrc_client = None
+        self.assertEqual(1, self.xfr.command_handler("retransfer",
+                                                     self.args)['result'][0])
+
+        # list.find() raises an exception
+        self.xfr._datasrc_clients_mgr.found_datasrc_client = \
+            isc.datasrc.Error('test exception')
+        self.assertEqual(1, self.xfr.command_handler("retransfer",
+                                                     self.args)['result'][0])
+
+        # datasrc.find() raises an exception
+        class RaisingkDataSourceClient(MockDataSourceClient):
+            def find_zone(self, zone_name):
+                raise isc.datasrc.Error('test exception')
+        self.xfr._datasrc_clients_mgr.found_datasrc_client = \
+            RaisingkDataSourceClient()
+        self.assertEqual(1, self.xfr.command_handler("retransfer",
+                                                     self.args)['result'][0])
 
     def test_command_handler_refresh(self):
         # at this level, refresh is no different than retransfer.
@@ -3001,133 +3032,14 @@ class TestXfrin(unittest.TestCase):
         self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
                          self.xfr.xfrin_started_request_ixfr)
 
-class TestXfrinMemoryZones(unittest.TestCase):
-    def setUp(self):
-        self.xfr = MockXfrin()
-        # Configuration snippet containing 2 memory datasources,
-        # one for IN and one for CH. Both contain a zone 'example.com'
-        # the IN ds also contains a zone example2.com, and a zone example3.com,
-        # which is of file type 'text' (and hence, should be ignored)
-        self.config = { 'datasources': [
-                          { 'type': 'memory',
-                            'class': 'IN',
-                            'zones': [
-                              { 'origin': 'example.com',
-                                'filetype': 'sqlite3' },
-                              { 'origin': 'EXAMPLE2.com.',
-                                'filetype': 'sqlite3' },
-                              { 'origin': 'example3.com',
-                                'filetype': 'text' }
-                            ]
-                          },
-                          { 'type': 'memory',
-                            'class': 'ch',
-                            'zones': [
-                              { 'origin': 'example.com',
-                                'filetype': 'sqlite3' }
-                            ]
-                          }
-                      ] }
-
-    def test_updates(self):
-        self.assertFalse(self.xfr._is_memory_zone("example.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example2.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example.com", "CH"))
-
-        # add them all
-        self.xfr._set_memory_zones(self.config, None)
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
-        self.assertTrue(self.xfr._is_memory_zone("example2.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "CH"))
-
-        # Remove the CH data source from the self.config snippet, and update
-        del self.config['datasources'][1]
-        self.xfr._set_memory_zones(self.config, None)
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
-        self.assertTrue(self.xfr._is_memory_zone("example2.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example.com", "CH"))
-
-        # Remove example2.com from the datasource, and update
-        del self.config['datasources'][0]['zones'][1]
-        self.xfr._set_memory_zones(self.config, None)
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example2.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example.com", "CH"))
-
-        # If 'datasources' is not in the self.config update list (i.e. its
-        # self.config has not changed), no difference should be found
-        self.xfr._set_memory_zones({}, None)
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example2.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example.com", "CH"))
-
-        # If datasources list becomes empty, everything should be removed
-        self.config['datasources'][0]['zones'] = []
-        self.xfr._set_memory_zones(self.config, None)
-        self.assertFalse(self.xfr._is_memory_zone("example.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example2.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example.com", "CH"))
-
-    def test_normalization(self):
-        self.xfr._set_memory_zones(self.config, None)
-        # make sure it is case insensitive, root-dot-insensitive,
-        # and supports CLASSXXX notation
-        self.assertTrue(self.xfr._is_memory_zone("EXAMPLE.com", "IN"))
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "in"))
-        self.assertTrue(self.xfr._is_memory_zone("example2.com.", "IN"))
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "CLASS3"))
-
-    def test_bad_name(self):
-        # First set it to some config
-        self.xfr._set_memory_zones(self.config, None)
-
-        # Error checking; bad owner name should result in no changes
-        self.config['datasources'][1]['zones'][0]['origin'] = ".."
-        self.xfr._set_memory_zones(self.config, None)
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
-        self.assertTrue(self.xfr._is_memory_zone("example2.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "CH"))
-
-    def test_bad_class(self):
-        # First set it to some config
-        self.xfr._set_memory_zones(self.config, None)
-
-        # Error checking; bad owner name should result in no changes
-        self.config['datasources'][1]['class'] = "Foo"
-        self.xfr._set_memory_zones(self.config, None)
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
-        self.assertTrue(self.xfr._is_memory_zone("example2.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "CH"))
-
-    def test_no_filetype(self):
-        # omitting the filetype should leave that zone out, but not
-        # the rest
-        del self.config['datasources'][1]['zones'][0]['filetype']
-        self.xfr._set_memory_zones(self.config, None)
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
-        self.assertTrue(self.xfr._is_memory_zone("example2.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example.com", "CH"))
-
-    def test_class_filetype(self):
-        # omitting the class should have it default to what is in the
-        # specfile for Auth.
-        AuthConfigData = isc.config.config_data.ConfigData(
-            isc.config.module_spec_from_file(xfrin.AUTH_SPECFILE_LOCATION))
-        del self.config['datasources'][0]['class']
-        self.xfr._set_memory_zones(self.config, AuthConfigData)
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
-        self.assertTrue(self.xfr._is_memory_zone("example2.com", "IN"))
-        self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
-        self.assertTrue(self.xfr._is_memory_zone("example.com", "CH"))
+    def test_datasrc_config_handler(self):
+        """Check datasrc config handler works expectedly."""
+        # This is a simple wrapper of DataSrcClientsMgr.reconfigure(), so
+        # we just check it's called as expected, and the only possible
+        # exception doesn't cause disruption.
+        self.xfr._datasrc_config_handler(True, False)
+        self.assertEqual([True],
+                         self.xfr._datasrc_clients_mgr.reconfigure_param)
 
 def raise_interrupt():
     raise KeyboardInterrupt()
@@ -3273,7 +3185,8 @@ class TestXfrinProcess(unittest.TestCase):
         """
         pass
 
-    def __do_test(self, rets, transfers, request_ixfr):
+    def __do_test(self, rets, transfers, request_ixfr,
+                  zone_soa=begin_soa_rrset):
         """
         Do the actual test. The request type, prepared sucesses/failures
         and expected sequence of transfers is passed to specify what test
@@ -3282,8 +3195,9 @@ class TestXfrinProcess(unittest.TestCase):
         self.__rets = rets
         published = rets[-1]
         xfrin.process_xfrin(self, XfrinRecorder(), Name("example.org."),
-                            RRClass.IN, None, None, TEST_MASTER_IPV4_ADDRINFO,
-                            True, None, request_ixfr, self.__get_connection)
+                            RRClass.IN, None, zone_soa, None,
+                            TEST_MASTER_IPV4_ADDRINFO, True, None,
+                            request_ixfr, self.__get_connection)
         self.assertEqual([], self.__rets)
         self.assertEqual(transfers, self.__transfers)
         # Create a connection for each attempt
@@ -3365,9 +3279,6 @@ class TestXfrinProcess(unittest.TestCase):
             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 = []
@@ -3380,7 +3291,7 @@ class TestXfrinProcess(unittest.TestCase):
                     expected_type = RRType.AXFR
 
                 # perform the test
-                self.__do_test([XFRIN_OK], [expected_type], request_ixfr)
+                self.__do_test([XFRIN_OK], [expected_type], request_ixfr, soa)
 
 class TestFormatting(unittest.TestCase):
     # If the formatting functions are moved to a more general library
diff --git a/src/bin/xfrin/xfrin.py.in b/src/bin/xfrin/xfrin.py.in
index f127a93..bcf96af 100755
--- a/src/bin/xfrin/xfrin.py.in
+++ b/src/bin/xfrin/xfrin.py.in
@@ -37,6 +37,7 @@ import isc.net.parse
 from isc.xfrin.diff import Diff
 from isc.server_common.auth_command import auth_loadzone_command
 from isc.server_common.tsig_keyring import init_keyring, get_keyring
+from isc.server_common.datasrc_clients_mgr import DataSrcClientsMgr, ConfigError
 from isc.log_messages.xfrin_messages import *
 from isc.dns import *
 
@@ -56,13 +57,9 @@ isc.util.process.rename()
 SPECFILE_PATH = "@datadir@/@PACKAGE@"\
     .replace("${datarootdir}", "@datarootdir@")\
     .replace("${prefix}", "@prefix@")
-AUTH_SPECFILE_PATH = SPECFILE_PATH
 if "B10_FROM_SOURCE" in os.environ:
     SPECFILE_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/xfrin"
-if "B10_FROM_BUILD" in os.environ:
-    AUTH_SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/auth"
 SPECFILE_LOCATION = SPECFILE_PATH + "/xfrin.spec"
-AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + "/auth.spec"
 
 AUTH_MODULE_NAME = 'Auth'
 XFROUT_MODULE_NAME = 'Xfrout'
@@ -1074,62 +1071,6 @@ class XfrinConnection(asyncore.dispatcher):
 
         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.
 
@@ -1153,32 +1094,14 @@ def __get_initial_xfr_type(zone_soa, request_ixfr, zname, zclass, master_addr):
                      AddressFormatter(master_addr))
     return RRType.IXFR
 
-def __process_xfrin(server, zone_name, rrclass, db_file,
+def __process_xfrin(server, zone_name, rrclass, datasrc_client, zone_soa,
                     shutdown_event, master_addrinfo, check_soa, tsig_key,
                     request_ixfr, conn_class):
     conn = None
     exception = None
     ret = XFRIN_FAIL
     try:
-        # 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.
-        # For a temporary workaround to deal with these situations, we skip the
-        # creation when the given file is none (the test case).  Eventually
-        # this code will be much cleaner.
-        datasrc_client = None
-        if db_file is not None:
-            # temporary hardcoded sqlite initialization. Once we decide on
-            # the config specification, we need to update this (TODO)
-            # this may depend on #1207, or any follow-up ticket created for
-            # #1207
-            datasrc_type = "sqlite3"
-            datasrc_config = "{ \"database_file\": \"" + db_file + "\"}"
-            datasrc_client = DataSourceClient(datasrc_type, datasrc_config)
-
-        # 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)
+        # Determine the initialreuqest type: AXFR or IXFR.
         request_type = __get_initial_xfr_type(zone_soa, request_ixfr,
                                               zone_name, rrclass,
                                               master_addrinfo[2])
@@ -1242,9 +1165,9 @@ def __process_xfrin(server, zone_name, rrclass, db_file,
     if exception is not None:
         raise exception
 
-def process_xfrin(server, xfrin_recorder, zone_name, rrclass, db_file,
-                  shutdown_event, master_addrinfo, check_soa, tsig_key,
-                  request_ixfr, conn_class=XfrinConnection):
+def process_xfrin(server, xfrin_recorder, zone_name, rrclass, datasrc_client,
+                  zone_soa, shutdown_event, master_addrinfo, check_soa,
+                  tsig_key, request_ixfr, conn_class=XfrinConnection):
     # 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
     # is released in any cases, we delegate the main part to the helper
@@ -1252,7 +1175,7 @@ def process_xfrin(server, xfrin_recorder, zone_name, rrclass, db_file,
     xfrin_recorder.increment(zone_name)
     exception = None
     try:
-        __process_xfrin(server, zone_name, rrclass, db_file,
+        __process_xfrin(server, zone_name, rrclass, datasrc_client, zone_soa,
                         shutdown_event, master_addrinfo, check_soa, tsig_key,
                         request_ixfr, conn_class)
     except Exception as ex:
@@ -1449,15 +1372,22 @@ class Xfrin:
     def __init__(self):
         self._max_transfers_in = 10
         self._zones = {}
-        # This is a set of (zone/class) tuples (both as strings),
-        # representing the in-memory zones maintaned by Xfrin. It
-        # is used to trigger Auth/in-memory so that it reloads
-        # zones when they have been transfered in
-        self._memory_zones = set()
-        self._cc_setup()
         self.recorder = XfrinRecorder()
         self._shutdown_event = threading.Event()
         self._counters = Counters(SPECFILE_LOCATION)
+        # This is essentially private, but we allow tests to customize it.
+        self._datasrc_clients_mgr = DataSrcClientsMgr()
+
+        # Initial configuration
+        self._cc_setup()
+        config_data = self._module_cc.get_full_config()
+        self.config_handler(config_data)
+        # data_sources configuration should be ready with cfgmgr, so this
+        # shouldn't fail; if it ever does we simply propagate the exception
+        # to terminate the program.
+        self._module_cc.add_remote_config_by_name('data_sources',
+                                                  self._datasrc_config_handler)
+        init_keyring(self._module_cc)
 
     def _cc_setup(self):
         '''This method is used only as part of initialization, but is
@@ -1468,14 +1398,9 @@ class Xfrin:
         # listening session will block the send operation.
         self._send_cc_session = isc.cc.Session()
         self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
-                                              self.config_handler,
-                                              self.command_handler)
+                                                     self.config_handler,
+                                                     self.command_handler)
         self._module_cc.start()
-        config_data = self._module_cc.get_full_config()
-        self.config_handler(config_data)
-        self._module_cc.add_remote_config(AUTH_SPECFILE_LOCATION,
-                                          self._auth_config_handler)
-        init_keyring(self._module_cc)
 
     def _cc_check_command(self):
         '''This is a straightforward wrapper for cc.check_command,
@@ -1507,7 +1432,8 @@ class Xfrin:
         old_max_transfers_in = self._max_transfers_in
         old_zones = self._zones
 
-        self._max_transfers_in = new_config.get("transfers_in") or self._max_transfers_in
+        self._max_transfers_in = \
+            new_config.get("transfers_in") or self._max_transfers_in
 
         if 'zones' in new_config:
             self._clear_zone_info()
@@ -1522,78 +1448,25 @@ class Xfrin:
 
         return create_answer(0)
 
-    def _auth_config_handler(self, new_config, config_data):
-        # Config handler for changes in Auth configuration
-        self._set_db_file()
-        self._set_memory_zones(new_config, config_data)
-
-    def _clear_memory_zones(self):
-        """Clears the memory_zones set; called before processing the
-           changed list of memory datasource zones that have file type
-           sqlite3"""
-        self._memory_zones.clear()
-
-    def _is_memory_zone(self, zone_name_str, zone_class_str):
-        """Returns true if the given zone/class combination is configured
-           in the in-memory datasource of the Auth process with file type
-           'sqlite3'.
-           Note: this method is not thread-safe. We are considering
-           changing the threaded model here, but if we do not, take
-           care in accessing and updating the memory zone set (or add
-           locks)
-        """
-        # Normalize them first, if either conversion fails, return false
-        # (they won't be in the set anyway)
-        try:
-            zone_name_str = Name(zone_name_str).to_text().lower()
-            zone_class_str = RRClass(zone_class_str).to_text()
-        except Exception:
-            return False
-        return (zone_name_str, zone_class_str) in self._memory_zones
-
-    def _set_memory_zones(self, new_config, config_data):
-        """Part of the _auth_config_handler function, keeps an internal set
-           of zones in the datasources config subset that have 'sqlite3' as
-           their file type.
-           Note: this method is not thread-safe. We are considering
-           changing the threaded model here, but if we do not, take
-           care in accessing and updating the memory zone set (or add
-           locks)
+    def _datasrc_config_handler(self, new_config, config_data):
+        """Configuration handler of the 'data_sources' module.
+
+        The actual handling is deletegated to the DataSrcClientsMgr class;
+        this method is a simple wrapper.
+
+        This is essentially private, but implemented as 'protected' so tests
+        can refer to it; other external use is prohibited.
+
         """
-        # walk through the data and collect the memory zones
-        # If this causes any exception, assume we were passed bad data
-        # and keep the original set
-        new_memory_zones = set()
         try:
-            if "datasources" in new_config:
-                for datasource in new_config["datasources"]:
-                    if "class" in datasource:
-                        ds_class = RRClass(datasource["class"])
-                    else:
-                        # Get the default
-                        ds_class = RRClass(config_data.get_default_value(
-                                               "datasources/class"))
-                    if datasource["type"] == "memory":
-                        for zone in datasource["zones"]:
-                            if "filetype" in zone and \
-                               zone["filetype"] == "sqlite3":
-                                zone_name = Name(zone["origin"])
-                                zone_name_str = zone_name.to_text().lower()
-                                new_memory_zones.add((zone_name_str,
-                                                      ds_class.to_text()))
-                # Ok, we can use the data, update our list
-                self._memory_zones = new_memory_zones
-        except Exception:
-            # Something is wrong with the data. If this data even reached us,
-            # we cannot do more than assume the real module has logged and
-            # reported an error. Keep the old set.
-            return
+            self._datasrc_clients_mgr.reconfigure(new_config)
+        except isc.server_common.datasrc_clients_mgr.ConfigError as ex:
+            logger.error(XFRIN_DATASRC_CONFIG_ERROR, ex)
 
     def shutdown(self):
         ''' shutdown the xfrin process. the thread which is doing xfrin should be
         terminated.
         '''
-        self._module_cc.remove_remote_config(AUTH_SPECFILE_LOCATION)
         self._module_cc.send_stopping()
         self._shutdown_event.set()
         main_thread = threading.currentThread()
@@ -1646,7 +1519,7 @@ class Xfrin:
             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,
+    def __handle_xfr_command(self, args, check_soa, addr_validator,
                              request_ixfr):
         """Common subroutine for handling transfer commands.
 
@@ -1672,14 +1545,13 @@ class Xfrin:
         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)
+        ret = self.xfrin_start(zone_name, rrclass, master_addr, tsig_key,
+                               request_ixfr, check_soa)
         return create_answer(ret[0], ret[1])
 
     def command_handler(self, command, args):
@@ -1690,25 +1562,23 @@ class Xfrin:
                 self._shutdown_event.set()
             elif command == 'notify' or command == REFRESH_FROM_ZONEMGR:
                 # refresh/notify command from zone manager.
-                # The address has to be validated, db_file is local only,
-                # and always perform SOA check.
+                # The address has to be validated 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)
+                answer = self.__handle_xfr_command(args, 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.
+                # No need for address validation, skip SOA check, and always
+                # use AXFR.
                 answer = self.__handle_xfr_command(
-                    args, args.get('db_file'), False, lambda x, y, z: None,
+                    args, 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)
+                    args, True, lambda x, y, z: None, None)
             # return statistics data to the stats daemon
             elif command == "getstats":
                 # The log level is here set to debug in order to avoid
@@ -1772,21 +1642,6 @@ class Xfrin:
 
         return (addr.family, socket.SOCK_STREAM, (str(addr), port))
 
-    def _get_db_file(self):
-        return self._db_file
-
-    def _set_db_file(self):
-        db_file, is_default =\
-            self._module_cc.get_remote_config_value(AUTH_MODULE_NAME, "database_file")
-        if is_default and "B10_FROM_BUILD" in os.environ:
-            # override the local database setting if it is default and we
-            # are running from the source tree
-            # This should be hidden inside the data source library and/or
-            # done as a configuration, and this special case should be gone).
-            db_file = os.environ["B10_FROM_BUILD"] + os.sep +\
-                      "bind10_zones.sqlite3"
-        self._db_file = db_file
-
     def publish_xfrin_news(self, zone_name, zone_class, xfr_result):
         '''Send command to xfrout/zone manager module.
         If xfrin has finished successfully for one zone, tell the good
@@ -1846,11 +1701,8 @@ class Xfrin:
         while not self._shutdown_event.is_set():
             self._cc_check_command()
 
-    def xfrin_start(self, zone_name, rrclass, db_file, master_addrinfo,
-                    tsig_key, request_ixfr, check_soa=True):
-        if "pydnspp" not in sys.modules:
-            return (1, "xfrin failed, can't load dns message python library: 'pydnspp'")
-
+    def xfrin_start(self, zone_name, rrclass, master_addrinfo, tsig_key,
+                    request_ixfr, check_soa=True):
         # check max_transfer_in, else return quota error
         if self.recorder.count() >= self._max_transfers_in:
             return (1, 'xfrin quota error')
@@ -1858,19 +1710,82 @@ class Xfrin:
         if self.recorder.xfrin_in_progress(zone_name):
             return (1, 'zone xfrin is in progress')
 
-        xfrin_thread = threading.Thread(target = process_xfrin,
-                                        args = (self,
-                                                self.recorder,
-                                                zone_name,
-                                                rrclass,
-                                                db_file,
-                                                self._shutdown_event,
-                                                master_addrinfo, check_soa,
-                                                tsig_key, request_ixfr))
+        # Identify the data source to which the zone content is transferred,
+        # and get the current zone SOA from the data source (if available).
+        # Note that we do this before spawning the xfrin session thread.
+        # find() on the client list and use of ZoneFinder (in _get_zone_soa())
+        # should be completed within the same single thread.
+        datasrc_client = None
+        clist = self._datasrc_clients_mgr.get_client_list(rrclass)
+        if clist is None:
+            return (1, 'no data source is configured for class %s' % rrclass)
+
+        try:
+            datasrc_client = clist.find(zone_name, True, False)[0]
+            if datasrc_client is None: # can happen, so log it separately.
+                logger.error(XFRIN_DATASRC_UNKNOWN,
+                             format_zone_str(zone_name, rrclass))
+                return (1, 'data source to transfer %s to is unknown' %
+                        format_zone_str(zone_name, rrclass))
+            zone_soa = _get_zone_soa(datasrc_client, zone_name, rrclass)
+        except isc.datasrc.Error as ex:
+            # rare case error. re-raise as XfrinException so it'll be logged
+            # in command_handler().
+            raise XfrinException('unexpected failure in datasrc module: ' +
+                                 str(ex))
+
+        xfrin_thread = threading.Thread(target=process_xfrin,
+                                        args=(self, self.recorder,
+                                              zone_name, rrclass,
+                                              datasrc_client, zone_soa,
+                                              self._shutdown_event,
+                                              master_addrinfo, check_soa,
+                                              tsig_key, request_ixfr))
 
         xfrin_thread.start()
         return (0, 'zone xfrin is started')
 
+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.
+
+    The specified zone is expected to exist in the data source referenced
+    by the given datasrc_client at the point of the call to this function.
+    If this is not met XfrinException exception will be raised.
+
+    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.
+
+    """
+    # 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.  In the context of this
+        # function is called, this shouldn't happen.
+        raise XfrinException("unexpected result: zone %s doesn't exist" %
+                             format_zone_str(zone_name, zone_class))
+    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
 
 xfrind = None
 
diff --git a/src/bin/xfrin/xfrin_messages.mes b/src/bin/xfrin/xfrin_messages.mes
index 33a2193..9e4281a 100644
--- a/src/bin/xfrin/xfrin_messages.mes
+++ b/src/bin/xfrin/xfrin_messages.mes
@@ -60,6 +60,27 @@ error is given in the log message.
 There was an error opening a connection to the master. The error is
 shown in the log message.
 
+% XFRIN_DATASRC_CONFIG_ERROR failed to update data source configuration: %1
+Configuration for the global data sources is updated, but the update
+cannot be applied to xfrin.  The xfrin module will still keep running
+with the previous configuration, but the cause of the failure and
+other log messages must be carefully examined because if only xfrin
+rejects the new configuration then the entire BIND 10 system will have
+inconsistent state among different modules.  If other modules accept
+the update but xfrin produces this error, the xfrin module should
+probably be restarted.
+
+% XFRIN_DATASRC_UNKNOWN data source to transfer %1 to is unknown
+The xfrin daemon received a command that would trigger a transfer,
+but could not find a data source where the specified zone belongs.
+There can be several reasons for this error: it may be a simple
+misspelling in the xfrin or zonemgr configuration, or in the user
+supplied parameter if it is triggered by an external command (such as
+from bindctl).  Another possibility is that this is the initial transfer
+for a newly setup secondary zone.  In this case at least an initial empty zone
+must be created in one of configured data sources.  This can be done by
+the -e option of b10-loadzone.
+
 % XFRIN_EXITING exiting
 The xfrin daemon is exiting.
 
@@ -255,19 +276,6 @@ is recommended to check the primary server configuration.
 A connection to the master server has been made, the serial value in
 the SOA record has been checked, and a zone transfer has been started.
 
-% XFRIN_ZONE_CREATED Zone %1 not found in the given data source, newly created
-On starting an xfrin session, it is identified that the zone to be
-transferred is not found in the data source.  This can happen if a
-secondary DNS server first tries to perform AXFR from a primary server
-without creating the zone image beforehand (e.g. by b10-loadzone).  As
-of this writing the xfrin process provides backward compatible
-behavior to previous versions: creating a new one in the data source
-not to surprise existing users too much.  This is probably not a good
-idea, however, in terms of who should be responsible for managing
-zones at a higher level.  In future it is more likely that a separate
-zone management framework is provided, and the situation where the
-given zone isn't found in xfrout will be treated as an error.
-
 % XFRIN_ZONE_INVALID Newly received zone %1/%2 fails validation: %3
 The zone was received successfully, but it failed validation. The problem
 is severe enough that the new version of zone is discarded and the old version,
diff --git a/src/lib/python/isc/server_common/Makefile.am b/src/lib/python/isc/server_common/Makefile.am
index d89df2f..596d6cd 100644
--- a/src/lib/python/isc/server_common/Makefile.am
+++ b/src/lib/python/isc/server_common/Makefile.am
@@ -1,6 +1,7 @@
 SUBDIRS = tests
 
 python_PYTHON = __init__.py tsig_keyring.py auth_command.py dns_tcp.py
+python_PYTHON += datasrc_clients_mgr.py
 python_PYTHON += logger.py
 
 pythondir = $(pyexecdir)/isc/server_common
diff --git a/src/lib/python/isc/server_common/datasrc_clients_mgr.py b/src/lib/python/isc/server_common/datasrc_clients_mgr.py
new file mode 100644
index 0000000..75e6827
--- /dev/null
+++ b/src/lib/python/isc/server_common/datasrc_clients_mgr.py
@@ -0,0 +1,135 @@
+# Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import isc.dns
+import isc.datasrc
+import threading
+import json
+
+class ConfigError(Exception):
+    """Exception class raised for data source configuration errors."""
+    pass
+
+class DataSrcClientsMgr:
+    """A container of data source client lists.
+
+    This class represents a set of isc.datasrc.ConfigurableClientList
+    objects (currently per RR class), and provides APIs to configure
+    the lists and access to a specific list in a thread safe manner.
+
+    It is intended to be used by applications that refer to the global
+    'data_sources' module.  The reconfigure() method can be called from
+    a configuration callback for the module of the application.  The
+    get_client_list() method is a simple search method to get the configured
+    ConfigurableClientList object for a specified RR class (if any),
+    while still allowing a separate thread to reconfigure the entire lists.
+
+    """
+    def __init__(self, use_cache=False):
+        """Constructor.
+
+        In the initial implementation, user applications of this class are
+        generally expected to NOT use in-memory cache; use_cache would be
+        set to True only for tests.  In future, some applications such as
+        outbound zone transfer may want to set it to True.
+
+        Parameter:
+          use_cache (bool): If set to True, enable in-memory cache on
+                            (re)configuration.
+
+        """
+        self.__use_cache = use_cache
+
+        # Map from RRClass to ConfigurableClientList.  Resetting this map
+        # is protected by __map_lock.  Note that this lock doesn't protect
+        # "updates" of the map content (currently it's not a problem, but
+        # if and when we support more operations such as reloading
+        # particular zones in in-memory cache, remember that there will have
+        # to be an additional layer of protection).
+        self.__clients_map = {}
+        self.__map_lock = threading.Lock()
+
+    def get_client_list(self, rrclass):
+        """Return the configured ConfigurableClientList for the RR class.
+
+        If no client list is configured for the specified RR class, it
+        returns None.
+
+        This method should not raise an exception as long as the parameter
+        is of valid type.
+
+        This method can be safely called from a thread even if a different
+        thread is calling reconfigure().  Also, it's safe for the caller
+        to use the returned list even if reconfigure() is called while or
+        after the call to this thread.
+
+        Note that this class does not protect further access to the returned
+        list from multiple threads; it's the caller's responsbility to make
+        such access thread safe.  In general, the find() method on the list
+        and the use of ZoneFinder created by a DataSourceClient in the list
+        cannot be done by multiple threads without explicit synchronization.
+        On the other hand, multiple threads can create and use ZoneUpdater,
+        ZoneIterator, or ZoneJournalReader on a DataSourceClient in parallel.
+
+        Parameter:
+          rrclass (isc.dns.RRClass): the RR class of the ConfigurableClientList
+            to be returned.
+        """
+        with self.__map_lock:
+            client_list = self.__clients_map.get(rrclass)
+        return client_list
+
+    def reconfigure(self, config):
+        """(Re)configure the set of client lists.
+
+        This method takes a new set of data source configuration, builds
+        a new set of ConfigurableClientList objects corresponding to the
+        configuration, and replaces the internal set with the newly built
+        one.  Its parameter is expected to be the "new configuration"
+        parameter of a configuration update callback for the global
+        "data_sources" module.  It should match the configuration data
+        of the module spec (see the datasrc.spec file).
+
+        Any error in reconfiguration is converted to a ConfigError
+        exception and is raised from the method.  This method guarantees
+        strong exception safety: unless building a new set for the new
+        configuration is fully completed, the old set is intact.
+
+        This method can be called from a thread while some other thread
+        is calling get_client_list() and using the result (see
+        the description of get_client_list()).  In general, however,
+        only one thread can call this method at one time; while data
+        integrity will still be preserved, the ordering of the change
+        will not be guaranteed if multiple threads call this method
+        at the same time.
+
+        Parameter:
+          config (dict): configuration data for the data_sources module.
+
+        """
+        try:
+            new_map = {}
+            for rrclass_cfg, class_cfg in config.get('classes').items():
+                rrclass = isc.dns.RRClass(rrclass_cfg)
+                new_client_list = isc.datasrc.ConfigurableClientList(rrclass)
+                new_client_list.configure(json.dumps(class_cfg),
+                                          self.__use_cache)
+                new_map[rrclass] = new_client_list
+            with self.__map_lock:
+                self.__clients_map = new_map
+        except Exception as ex:
+            # Catch all types of exceptions as a whole: there won't be much
+            # granularity for exceptions raised from the C++ module anyway.
+            raise ConfigError(ex)
diff --git a/src/lib/python/isc/server_common/tests/Makefile.am b/src/lib/python/isc/server_common/tests/Makefile.am
index fff57d6..86006d5 100644
--- a/src/lib/python/isc/server_common/tests/Makefile.am
+++ b/src/lib/python/isc/server_common/tests/Makefile.am
@@ -1,5 +1,5 @@
 PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-PYTESTS = tsig_keyring_test.py dns_tcp_test.py
+PYTESTS = tsig_keyring_test.py dns_tcp_test.py datasrc_clients_mgr_test.py
 EXTRA_DIST = $(PYTESTS)
 
 # If necessary (rare cases), explicitly specify paths to dynamic libraries
@@ -9,6 +9,14 @@ if SET_ENV_LIBRARY_PATH
 LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
 endif
 
+# We use our own "default" datasrc.spec, tweaking some installation path,
+# so we can run the tests with something very close to the actual spec and
+# yet independent from installation environment.
+BUILT_SOURCES = datasrc.spec
+datasrc.spec: $(abs_top_builddir)/src/bin/cfgmgr/plugins/datasrc.spec.pre
+	$(SED) -e "s|@@STATIC_ZONE_FILE@@|$(abs_top_builddir)/src/lib/datasrc/static.zone|;s|@@SQLITE3_DATABASE_FILE@@|$(abs_builddir)/zone.sqlite3|" $(abs_top_builddir)/src/bin/cfgmgr/plugins/datasrc.spec.pre >$@
+CLEANFILES = datasrc.spec zone.sqlite3
+
 # test using command-line arguments, so use check-local target instead of TESTS
 check-local:
 if ENABLE_PYTHON_COVERAGE
@@ -21,5 +29,6 @@ endif
 	$(LIBRARY_PATH_PLACEHOLDER) \
 	PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/lib/dns/python/.libs \
 	B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
+	B10_FROM_BUILD=$(abs_top_builddir) \
 	$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
 	done
diff --git a/src/lib/python/isc/server_common/tests/datasrc_clients_mgr_test.py b/src/lib/python/isc/server_common/tests/datasrc_clients_mgr_test.py
new file mode 100644
index 0000000..827a417
--- /dev/null
+++ b/src/lib/python/isc/server_common/tests/datasrc_clients_mgr_test.py
@@ -0,0 +1,110 @@
+# Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import isc.log
+from isc.server_common.datasrc_clients_mgr import *
+from isc.dns import *
+import unittest
+import isc.config
+import os
+
+# A (slightly tweaked) local copy of the default data source spec
+DATASRC_SPECFILE = os.environ["B10_FROM_BUILD"] + \
+    "/src/lib/python/isc/server_common/tests/datasrc.spec"
+DEFAULT_CONFIG = \
+    isc.config.ConfigData(isc.config.module_spec_from_file(DATASRC_SPECFILE)).\
+    get_full_config()
+
+class DataSrcClientsMgrTest(unittest.TestCase):
+    def setUp(self):
+        # We construct the manager with enabling in-memory cache for easier
+        # tests.  There should be no risk of inter-thread issues in the tests.
+        self.__mgr = DataSrcClientsMgr(use_cache=True)
+
+    def test_init(self):
+        """Check some initial state.
+
+        Initially there's no client list available, but get_client_list
+        doesn't cause disruption.
+        """
+        self.assertIsNone(self.__mgr.get_client_list(RRClass.IN))
+        self.assertIsNone(self.__mgr.get_client_list(RRClass.CH))
+
+    def test_reconfigure(self):
+        """Check configuration behavior.
+
+        First try the default configuration, and replace it with something
+        else.
+        """
+
+        # There should be at least in-memory only data for the static
+        # bind/CH zone. (We don't assume the existence of SQLite3 datasrc,
+        # so it'll still work if and when we make the default DB-independent).
+        self.__mgr.reconfigure(DEFAULT_CONFIG)
+        clist = self.__mgr.get_client_list(RRClass.CH)
+        self.assertIsNotNone(clist)
+        self.assertTrue(clist.find(Name('bind'), True, False)[2])
+
+        # Reconfigure it with a simple new config: the list for CH will be
+        # gone, and and an empty list for IN will be installed.
+        self.__mgr.reconfigure({"classes": {"IN": []}})
+        self.assertIsNone(self.__mgr.get_client_list(RRClass.CH))
+        self.assertIsNotNone(self.__mgr.get_client_list(RRClass.IN))
+
+    def test_reconfigure_error(self):
+        """Check reconfigure failure preserves the old config."""
+        # Configure it with the default
+        self.__mgr.reconfigure(DEFAULT_CONFIG)
+        self.assertIsNotNone(self.__mgr.get_client_list(RRClass.CH))
+
+        # Then try invalid configuration
+        self.assertRaises(ConfigError, self.__mgr.reconfigure, 42)
+        self.assertIsNotNone(self.__mgr.get_client_list(RRClass.CH))
+
+        # Another type of invalid configuration: exception would come from
+        # the C++ wrapper.
+        self.assertRaises(ConfigError,
+                          self.__mgr.reconfigure, {"classes": {"IN": 42}})
+        self.assertIsNotNone(self.__mgr.get_client_list(RRClass.CH))
+
+    def test_reconfig_while_using_old(self):
+        """Check datasrc client and finder can work even after list is gone."""
+        self.__mgr.reconfigure(DEFAULT_CONFIG)
+        clist = self.__mgr.get_client_list(RRClass.CH)
+        self.__mgr.reconfigure({"classes": {"IN": []}})
+
+        datasrc_client, finder, exact = clist.find(Name('bind'))
+        self.assertTrue(exact)
+
+        # Reset the client list
+        clist = None
+
+        # Both finder and datasrc client should still work without causing
+        # disruption.  We shouldn't have to inspect too much details of the
+        # returned values.
+        result, rrset, _ = finder.find(Name('bind'), RRType.SOA)
+        self.assertEqual(Name('bind'), rrset.get_name())
+        self.assertEqual(RRType.SOA, rrset.get_type())
+        self.assertEqual(RRClass.CH, rrset.get_class())
+        self.assertEqual(RRTTL(0), rrset.get_ttl())
+
+        # iterator should produce some non empty set of RRsets
+        rrsets = datasrc_client.get_iterator(Name('bind'))
+        self.assertNotEqual(0, len(list(rrsets)))
+
+if __name__ == "__main__":
+    isc.log.init("bind10")
+    isc.log.resetUnitTestRootLogger()
+    unittest.main()
diff --git a/tests/lettuce/features/terrain/loadzone.py b/tests/lettuce/features/terrain/loadzone.py
index 2cf550c..32fc82b 100644
--- a/tests/lettuce/features/terrain/loadzone.py
+++ b/tests/lettuce/features/terrain/loadzone.py
@@ -25,12 +25,16 @@ def run_loadzone(zone, zone_file, db_file):
 
     Parameters:
     zone (str): the zone name
-    zone_file (str): master zone file for the zone
+    zone_file (str): master zone file for the zone; can be None to make an
+                     empty zone.
     db_file (str): SQLite3 DB file to load the zone into
 
     """
     sqlite_datasrc_cfg = '{"database_file": "' + db_file + '"}'
-    args = ['b10-loadzone', '-c', sqlite_datasrc_cfg, zone, zone_file]
+    if zone_file is not None:
+        args = ['b10-loadzone', '-c', sqlite_datasrc_cfg, zone, zone_file]
+    else:
+        args = ['b10-loadzone', '-c', sqlite_datasrc_cfg, '-e', zone]
     loadzone = subprocess.Popen(args, 1, None, None,
                                 subprocess.PIPE, subprocess.PIPE)
     (stdout, stderr) = loadzone.communicate()
@@ -54,6 +58,22 @@ def load_zone_to_dbfile(step, zone, db_file, zone_file):
     """
     run_loadzone(zone, zone_file, db_file)
 
+ at step('make empty zone (\S+) in DB file (\S+)')
+def make_empty_zone_to_dbfile(step, zone, db_file):
+    """Make an empty zone into a data source.
+
+    If a non-empty zone already exists in the data source, it will be emptied;
+    otherwise, a new empty zone will be created.
+
+    It currently only works for an SQLite3-based data source.  Its
+    DB file name should be specified.
+
+    Step definition:
+    make empty zone <zone_name> to DB file <db_file>
+
+    """
+    run_loadzone(zone, None, db_file)
+
 @step('load (\d+) records for zone (\S+) to DB file (\S+)')
 def load_zone_rr_to_dbfile(step, num_records, zone, db_file):
     """Load a zone with a specified number of RRs into a data source.
diff --git a/tests/lettuce/features/xfrin_bind10.feature b/tests/lettuce/features/xfrin_bind10.feature
index a7d7f85..db4bddb 100644
--- a/tests/lettuce/features/xfrin_bind10.feature
+++ b/tests/lettuce/features/xfrin_bind10.feature
@@ -20,10 +20,13 @@ Feature: Xfrin
     And wait for bind10 stderr message XFRIN_STARTED
     And wait for bind10 stderr message ZONEMGR_STARTED
 
-    # Now we use the first step again to see if the file has been created
+    # Now we use the first step again to see if the file has been created.
+    # The DB currently doesn't know anything about the zone, so we install
+    # an empty zone for xfrin.
     The file data/test_nonexistent_db.sqlite3 should exist
-
     A query for www.example.org to [::1]:47806 should have rcode REFUSED
+    Then make empty zone example.org in DB file data/test_nonexistent_db.sqlite3
+
     When I send bind10 the command Xfrin retransfer example.org IN ::1 47807
     # The data we receive contain a NS RRset that refers to three names in the
     # example.org. zone. All these three are nonexistent in the data, producing
@@ -80,6 +83,9 @@ Feature: Xfrin
     And wait for bind10 stderr message CMDCTL_STARTED
     And wait for bind10 stderr message XFRIN_STARTED
 
+    # For xfrin make the data source aware of the zone (with empty data)
+    Then make empty zone example.org in DB file data/test_nonexistent_db.sqlite3
+
     # Set slave config for 'automatic' xfrin
     When I set bind10 configuration Xfrin/zones to [{"master_port": 47806, "name": "example.org", "master_addr": "::1"}]
 
@@ -135,10 +141,12 @@ Feature: Xfrin
     And wait for bind10 stderr message XFRIN_STARTED
     And wait for bind10 stderr message ZONEMGR_STARTED
 
-    # Now we use the first step again to see if the file has been created
+    # Now we use the first step again to see if the file has been created,
+    # then install empty zone data
     The file data/test_nonexistent_db.sqlite3 should exist
-
     A query for www.example.org to [::1]:47806 should have rcode REFUSED
+    Then make empty zone example.org in DB file data/test_nonexistent_db.sqlite3
+
     When I send bind10 the command Xfrin retransfer example.org IN ::1 47807
     # It should complain once about invalid data, then again that the whole
     # zone is invalid and then reject it.



More information about the bind10-changes mailing list