Browse Source

[master] Merge branch 'trac2967'

Paul Selkirk 11 years ago
parent
commit
4c9ef29631

+ 66 - 71
src/bin/zonemgr/tests/zonemgr_test.py

@@ -1,4 +1,4 @@
-# Copyright (C) 2010  Internet Systems Consortium.
+# Copyright (C) 2010-2013  Internet Systems Consortium.
 #
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
@@ -22,6 +22,7 @@ import tempfile
 from zonemgr import *
 from isc.testutils.ccsession_mock import MockModuleCCSession
 from isc.notify import notify_out
+from isc.datasrc import ZoneFinder
 
 ZONE_NAME_CLASS1_IN = ("example.net.", "IN")
 ZONE_NAME_CLASS1_CH = ("example.net.", "CH")
@@ -36,8 +37,6 @@ LOWERBOUND_RETRY = 5
 REFRESH_JITTER = 0.10
 RELOAD_JITTER = 0.75
 
-TEST_SQLITE3_DBFILE = os.getenv("TESTDATAOBJDIR") + '/initdb.file'
-
 class ZonemgrTestException(Exception):
     pass
 
@@ -46,16 +45,53 @@ class FakeCCSession(isc.config.ConfigData, MockModuleCCSession):
         module_spec = isc.config.module_spec_from_file(SPECFILE_LOCATION)
         ConfigData.__init__(self, module_spec)
         MockModuleCCSession.__init__(self)
+        # For inspection
+        self.added_remote_modules = []
+
+    def add_remote_config_by_name(self, name, callback):
+        self.added_remote_modules.append((name, callback))
 
     def rpc_call(self, command, module, instance="*", to="*", params=None):
         if module not in ("Auth", "Xfrin"):
             raise ZonemgrTestException("module name not exist")
 
-    def get_remote_config_value(self, module_name, identifier):
-        if module_name == "Auth" and identifier == "database_file":
-            return TEST_SQLITE3_DBFILE, False
+class MockDataSourceClient():
+    '''A simple mock data source client.'''
+    def __init__(self):
+        self.rdata_net = 'a.example.net. root.example.net. 2009073106 ' + \
+            '7200 3600 2419200 21600'
+        self.rdata_org = 'a.example.org. root.example.org. 2009073112 ' + \
+            '7200 3600 2419200 21600'
+
+    def find_zone(self, zone_name):
+        '''Mock version of DataSourceClient.find_zone().'''
+        return (isc.datasrc.DataSourceClient.SUCCESS, self)
+
+    def find(self, name, rrtype, options=ZoneFinder.FIND_DEFAULT):
+        '''Mock version of ZoneFinder.find().'''
+        if name == Name('example.net'):
+            rdata = Rdata(RRType.SOA, RRClass.IN, self.rdata_net)
+        elif name == 'example.org.':
+            rdata = Rdata(RRType.SOA, RRClass.IN, self.rdata_org)
         else:
-            return "unknown", False
+            return (ZoneFinder.NXDOMAIN, None, 0)
+        rrset = RRset(name, RRClass.IN, RRType.SOA, RRTTL(3600))
+        rrset.add_rdata(rdata)
+        return (ZoneFinder.SUCCESS, rrset, 0)
+
+class MockDataSrcClientsMgr():
+    '''A simple mock data source client manager.'''
+    def __init__(self):
+        self.datasrc_client = MockDataSourceClient()
+
+    def get_client_list(self, rrclass):
+        return self
+
+    def find(self, zone_name, want_exact_match, want_finder):
+        """Pretending find method on the object returned by get_client_list"""
+        if issubclass(type(self.datasrc_client), Exception):
+            raise self.datasrc_client
+        return self.datasrc_client, None, None
 
 class MyZonemgrRefresh(ZonemgrRefresh):
     def __init__(self):
@@ -66,19 +102,8 @@ class MyZonemgrRefresh(ZonemgrRefresh):
         self._reload_jitter = 0.75
         self._refresh_jitter = 0.25
 
-        def get_zone_soa(zone_name, db_file):
-            if zone_name == 'example.net.':
-                return (1, 2, 'example.net.', 'example.net.sd.', 21600, 'SOA', None,
-                        'a.example.net. root.example.net. 2009073106 7200 3600 2419200 21600')
-            elif zone_name == 'example.org.':
-                return (1, 2, 'example.org.', 'example.org.sd.', 21600, 'SOA', None,
-                        'a.example.org. root.example.org. 2009073112 7200 3600 2419200 21600')
-            else:
-                return None
-        sqlite3_ds.get_zone_soa = get_zone_soa
-
-        ZonemgrRefresh.__init__(self, TEST_SQLITE3_DBFILE, self._slave_socket,
-                                FakeCCSession())
+        ZonemgrRefresh.__init__(self, self._slave_socket, FakeCCSession())
+        self._datasrc_clients_mgr = MockDataSrcClientsMgr()
         current_time = time.time()
         self._zonemgr_refresh_info = {
          ('example.net.', 'IN'): {
@@ -95,19 +120,23 @@ class MyZonemgrRefresh(ZonemgrRefresh):
 
 class TestZonemgrRefresh(unittest.TestCase):
     def setUp(self):
-        if os.path.exists(TEST_SQLITE3_DBFILE):
-            os.unlink(TEST_SQLITE3_DBFILE)
         self.stderr_backup = sys.stderr
         sys.stderr = open(os.devnull, 'w')
         self.zone_refresh = MyZonemgrRefresh()
         self.cc_session = FakeCCSession()
 
     def tearDown(self):
-        if os.path.exists(TEST_SQLITE3_DBFILE):
-            os.unlink(TEST_SQLITE3_DBFILE)
         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.zone_refresh._datasrc_config_handler)],
+                         self.zone_refresh._module_cc.added_remote_modules)
+
     def test_random_jitter(self):
         max = 100025.120
         jitter = 0
@@ -195,16 +224,9 @@ class TestZonemgrRefresh(unittest.TestCase):
 
     def test_zonemgr_reload_zone(self):
         soa_rdata = 'a.example.net. root.example.net. 2009073106 1800 900 2419200 21600'
-        # We need to restore this not to harm other tests
-        old_get_zone_soa = sqlite3_ds.get_zone_soa
-        def get_zone_soa(zone_name, db_file):
-            return (1, 2, 'example.net.', 'example.net.sd.', 21600, 'SOA', None,
-                    'a.example.net. root.example.net. 2009073106 1800 900 2419200 21600')
-        sqlite3_ds.get_zone_soa = get_zone_soa
-
+        self.zone_refresh._datasrc_clients_mgr.datasrc_client.rdata_net = soa_rdata
         self.zone_refresh.zonemgr_reload_zone(ZONE_NAME_CLASS1_IN)
         self.assertEqual(soa_rdata, self.zone_refresh._zonemgr_refresh_info[ZONE_NAME_CLASS1_IN]["zone_soa_rdata"])
-        sqlite3_ds.get_zone_soa = old_get_zone_soa
 
     def test_get_zone_notifier_master(self):
         notify_master = "192.168.1.1"
@@ -275,24 +297,10 @@ class TestZonemgrRefresh(unittest.TestCase):
     def test_send_command(self):
         self.assertRaises(ZonemgrTestException, self.zone_refresh._send_command, "Unknown", "Notify", None)
 
-    def test_zone_mgr_is_empty(self):
-        self.assertFalse(self.zone_refresh._zone_mgr_is_empty())
-        self.zone_refresh._zonemgr_refresh_info = {}
-        self.assertTrue(self.zone_refresh._zone_mgr_is_empty())
-
     def test_zonemgr_add_zone(self):
         soa_rdata = 'a.example.net. root.example.net. 2009073106 1800 900 2419200 21600'
-        # This needs to be restored. The following test actually failed if we left
-        # this unclean
-        old_get_zone_soa = sqlite3_ds.get_zone_soa
+        self.zone_refresh._datasrc_clients_mgr.datasrc_client.rdata_net = soa_rdata
         time1 = time.time()
-
-        def get_zone_soa(zone_name, db_file):
-            return (1, 2, 'example.net.', 'example.net.sd.', 21600, 'SOA', None,
-                    'a.example.net. root.example.net. 2009073106 1800 900 2419200 21600')
-
-        sqlite3_ds.get_zone_soa = get_zone_soa
-
         self.zone_refresh._zonemgr_refresh_info = {}
         self.zone_refresh.zonemgr_add_zone(ZONE_NAME_CLASS1_IN)
         self.assertEqual(1, len(self.zone_refresh._zonemgr_refresh_info))
@@ -306,12 +314,13 @@ class TestZonemgrRefresh(unittest.TestCase):
         self.assertTrue((time1 + 900 * (1 - self.zone_refresh._reload_jitter)) <= zone_timeout)
         self.assertTrue(zone_timeout <= time2 + 900)
 
-        def get_zone_soa2(zone_name, db_file):
+        old_get_zone_soa = self.zone_refresh._get_zone_soa
+        def get_zone_soa2(zone_name_class):
             return None
-        sqlite3_ds.get_zone_soa = get_zone_soa2
+        self.zone_refresh._get_zone_soa = get_zone_soa2
         self.zone_refresh.zonemgr_add_zone(ZONE_NAME_CLASS2_IN)
         self.assertTrue(self.zone_refresh._zonemgr_refresh_info[ZONE_NAME_CLASS2_IN]["zone_soa_rdata"] is None)
-        sqlite3_ds.get_zone_soa = old_get_zone_soa
+        self.zone_refresh._get_zone_soa = old_get_zone_soa
 
     def test_zone_handle_notify(self):
         self.assertTrue(self.zone_refresh.zone_handle_notify(
@@ -333,10 +342,7 @@ class TestZonemgrRefresh(unittest.TestCase):
 
     def test_zone_refresh_success(self):
         soa_rdata = 'a.example.net. root.example.net. 2009073106 1800 900 2419200 21600'
-        def get_zone_soa(zone_name, db_file):
-            return (1, 2, 'example.net.', 'example.net.sd.', 21600, 'SOA', None,
-                    'a.example.net. root.example.net. 2009073106 1800 900 2419200 21600')
-        sqlite3_ds.get_zone_soa = get_zone_soa
+        self.zone_refresh._datasrc_clients_mgr.datasrc_client.rdata_net = soa_rdata
         time1 = time.time()
         self.zone_refresh._zonemgr_refresh_info[ZONE_NAME_CLASS1_IN]["zone_state"] = ZONE_REFRESHING
         self.zone_refresh.zone_refresh_success(ZONE_NAME_CLASS1_IN)
@@ -373,14 +379,14 @@ class TestZonemgrRefresh(unittest.TestCase):
         self.assertRaises(ZonemgrException, self.zone_refresh.zone_refresh_fail, ZONE_NAME_CLASS3_CH)
         self.assertRaises(ZonemgrException, self.zone_refresh.zone_refresh_fail, ZONE_NAME_CLASS3_IN)
 
-        old_get_zone_soa = sqlite3_ds.get_zone_soa
-        def get_zone_soa(zone_name, db_file):
+        old_get_zone_soa = self.zone_refresh._get_zone_soa
+        def get_zone_soa(zone_name_class):
             return None
-        sqlite3_ds.get_zone_soa = get_zone_soa
+        self.zone_refresh._get_zone_soa = get_zone_soa
         self.zone_refresh.zone_refresh_fail(ZONE_NAME_CLASS1_IN)
         self.assertEqual(self.zone_refresh._zonemgr_refresh_info[ZONE_NAME_CLASS1_IN]["zone_state"],
                          ZONE_EXPIRED)
-        sqlite3_ds.get_zone_soa = old_get_zone_soa
+        self.zone_refresh._get_zone_soa = old_get_zone_soa
 
     def test_find_need_do_refresh_zone(self):
         time1 = time.time()
@@ -628,7 +634,6 @@ class MyZonemgr(Zonemgr):
         def __exit__(self, type, value, traceback): pass
 
     def __init__(self):
-        self._db_file = TEST_SQLITE3_DBFILE
         self._zone_refresh = None
         self._shutdown_event = threading.Event()
         self._module_cc = FakeCCSession()
@@ -649,14 +654,8 @@ class MyZonemgr(Zonemgr):
 class TestZonemgr(unittest.TestCase):
 
     def setUp(self):
-        if os.path.exists(TEST_SQLITE3_DBFILE):
-            os.unlink(TEST_SQLITE3_DBFILE)
         self.zonemgr = MyZonemgr()
 
-    def tearDown(self):
-        if os.path.exists(TEST_SQLITE3_DBFILE):
-            os.unlink(TEST_SQLITE3_DBFILE)
-
     def test_config_handler(self):
         config_data1 = {
                     "lowerbound_refresh" : 60,
@@ -676,9 +675,8 @@ class TestZonemgr(unittest.TestCase):
         config_data3 = {"refresh_jitter" : 0.7}
         self.zonemgr.config_handler(config_data3)
         self.assertEqual(0.5, self.zonemgr._config_data.get("refresh_jitter"))
-        # The zone doesn't exist in database, simply skip loading soa for it and log an warning
-        self.zonemgr._zone_refresh = ZonemgrRefresh(TEST_SQLITE3_DBFILE, None,
-                                                    FakeCCSession())
+        # The zone doesn't exist in database, simply skip loading soa for it and log a warning
+        self.zonemgr._zone_refresh = ZonemgrRefresh(None, FakeCCSession())
         config_data1["secondary_zones"] = [{"name": "nonexistent.example",
                                             "class": "IN"}]
         self.assertEqual(self.zonemgr.config_handler(config_data1),
@@ -689,9 +687,6 @@ class TestZonemgr(unittest.TestCase):
                         is None)
         self.assertEqual(0.1, self.zonemgr._config_data.get("refresh_jitter"))
 
-    def test_get_db_file(self):
-        self.assertEqual(TEST_SQLITE3_DBFILE, self.zonemgr.get_db_file())
-
     def test_parse_cmd_params(self):
         params1 = {"zone_name" : "example.com.", "zone_class" : "CH",
                    "master" : "127.0.0.1"}

+ 166 - 86
src/bin/zonemgr/zonemgr.py.in

@@ -1,6 +1,6 @@
 #!@PYTHON@
 
-# Copyright (C) 2010  Internet Systems Consortium.
+# Copyright (C) 2010-2013  Internet Systems Consortium.
 #
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
@@ -34,12 +34,14 @@ import threading
 import select
 import socket
 import errno
-from isc.datasrc import sqlite3_ds
 from optparse import OptionParser, OptionValueError
 from isc.config.ccsession import *
 import isc.util.process
 from isc.log_messages.zonemgr_messages import *
 from isc.notify import notify_out
+from isc.server_common.datasrc_clients_mgr import DataSrcClientsMgr, ConfigError
+from isc.datasrc import DataSourceClient, ZoneFinder
+from isc.dns import *
 
 # Initialize logging for called modules.
 isc.log.init("b10-zonemgr", buffer=True)
@@ -66,7 +68,9 @@ if "B10_FROM_BUILD" in os.environ:
 else:
     PREFIX = "@prefix@"
     DATAROOTDIR = "@datarootdir@"
-    SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
+    SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}",
+                                          DATAROOTDIR).replace("${prefix}",
+                                                               PREFIX)
     AUTH_SPECFILE_PATH = SPECFILE_PATH
 
 SPECFILE_LOCATION = SPECFILE_PATH + "/zonemgr.spec"
@@ -76,7 +80,6 @@ __version__ = "BIND10"
 
 # define module name
 XFRIN_MODULE_NAME = 'Xfrin'
-AUTH_MODULE_NAME = 'Auth'
 
 # define command name
 ZONE_REFRESH_COMMAND = 'refresh_from_zonemgr'
@@ -103,19 +106,24 @@ class ZonemgrRefresh:
     can be stopped by calling shutdown() in another thread.
     """
 
-    def __init__(self, db_file, slave_socket, module_cc_session):
-        self._mccs = module_cc_session
+    def __init__(self, slave_socket, module_cc):
+        self._module_cc = module_cc
         self._check_sock = slave_socket
-        self._db_file = db_file
         self._zonemgr_refresh_info = {}
         self._lowerbound_refresh = None
         self._lowerbound_retry = None
         self._max_transfer_timeout = None
         self._refresh_jitter = None
         self._reload_jitter = None
-        self.update_config_data(module_cc_session.get_full_config(),
-                                module_cc_session)
+        self.update_config_data(module_cc.get_full_config(), module_cc)
         self._running = False
+        # This is essentially private, but we allow tests to customize it.
+        self._datasrc_clients_mgr = DataSrcClientsMgr()
+        # 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)
 
     def _random_jitter(self, max, jitter):
         """Imposes some random jitters for refresh and
@@ -133,27 +141,32 @@ class ZonemgrRefresh:
     def _set_zone_timer(self, zone_name_class, max, jitter):
         """Set zone next refresh time.
         jitter should not be bigger than half the original value."""
-        self._set_zone_next_refresh_time(zone_name_class, self._get_current_time() + \
+        self._set_zone_next_refresh_time(zone_name_class,
+                                         self._get_current_time() +
                                             self._random_jitter(max, jitter))
 
     def _set_zone_refresh_timer(self, zone_name_class):
         """Set zone next refresh time after zone refresh success.
            now + refresh - refresh_jitter <= next_refresh_time <= now + refresh
            """
-        zone_refresh_time = float(self._get_zone_soa_rdata(zone_name_class).split(" ")[REFRESH_OFFSET])
+        zone_refresh_time = float(self._get_zone_soa_rdata(zone_name_class).
+                                      split(" ")[REFRESH_OFFSET])
         zone_refresh_time = max(self._lowerbound_refresh, zone_refresh_time)
-        self._set_zone_timer(zone_name_class, zone_refresh_time, self._refresh_jitter * zone_refresh_time)
+        self._set_zone_timer(zone_name_class, zone_refresh_time,
+                             self._refresh_jitter * zone_refresh_time)
 
     def _set_zone_retry_timer(self, zone_name_class):
         """Set zone next refresh time after zone refresh fail.
            now + retry - retry_jitter <= next_refresh_time <= now + retry
            """
-        if (self._get_zone_soa_rdata(zone_name_class) is not None):
-            zone_retry_time = float(self._get_zone_soa_rdata(zone_name_class).split(" ")[RETRY_OFFSET])
+        if self._get_zone_soa_rdata(zone_name_class) is not None:
+            zone_retry_time = float(self._get_zone_soa_rdata(zone_name_class).
+                                        split(" ")[RETRY_OFFSET])
         else:
             zone_retry_time = 0.0
         zone_retry_time = max(self._lowerbound_retry, zone_retry_time)
-        self._set_zone_timer(zone_name_class, zone_retry_time, self._refresh_jitter * zone_retry_time)
+        self._set_zone_timer(zone_name_class, zone_retry_time,
+                             self._refresh_jitter * zone_retry_time)
 
     def _set_zone_notify_timer(self, zone_name_class):
         """Set zone next refresh time after receiving notify
@@ -167,19 +180,22 @@ class ZonemgrRefresh:
 
     def zone_refresh_success(self, zone_name_class):
         """Update zone info after zone refresh success"""
-        if (self._zone_not_exist(zone_name_class)):
-            logger.error(ZONEMGR_UNKNOWN_ZONE_SUCCESS, zone_name_class[0], zone_name_class[1])
+        if self._zone_not_exist(zone_name_class):
+            logger.error(ZONEMGR_UNKNOWN_ZONE_SUCCESS, zone_name_class[0],
+                         zone_name_class[1])
             raise ZonemgrException("[b10-zonemgr] Zone (%s, %s) doesn't "
                                    "belong to zonemgr" % zone_name_class)
         self.zonemgr_reload_zone(zone_name_class)
         self._set_zone_refresh_timer(zone_name_class)
         self._set_zone_state(zone_name_class, ZONE_OK)
-        self._set_zone_last_refresh_time(zone_name_class, self._get_current_time())
+        self._set_zone_last_refresh_time(zone_name_class,
+                                         self._get_current_time())
 
     def zone_refresh_fail(self, zone_name_class):
         """Update zone info after zone refresh fail"""
-        if (self._zone_not_exist(zone_name_class)):
-            logger.error(ZONEMGR_UNKNOWN_ZONE_FAIL, zone_name_class[0], zone_name_class[1])
+        if self._zone_not_exist(zone_name_class):
+            logger.error(ZONEMGR_UNKNOWN_ZONE_FAIL, zone_name_class[0],
+                         zone_name_class[1])
             raise ZonemgrException("[b10-zonemgr] Zone (%s, %s) doesn't "
                                    "belong to zonemgr" % zone_name_class)
         # Is zone expired?
@@ -199,11 +215,6 @@ class ZonemgrRefresh:
         zone; the Auth module should have rejected the case where it's not
         even authoritative for the zone.
 
-        Note: to be more robust and less independent from other module's
-        behavior, it's probably safer to check the authority condition here,
-        too.  But right now it uses SQLite3 specific API (to be deprecated),
-        so we rather rely on Auth.
-
         Parameters:
         zone_name_class (Name, RRClass): the notified zone name and class.
         master (str): textual address of the NOTIFY sender.
@@ -219,35 +230,86 @@ class ZonemgrRefresh:
 
     def zonemgr_reload_zone(self, zone_name_class):
         """ Reload a zone."""
-        zone_soa = sqlite3_ds.get_zone_soa(str(zone_name_class[0]), self._db_file)
-        self._zonemgr_refresh_info[zone_name_class]["zone_soa_rdata"] = zone_soa[7]
+        self._zonemgr_refresh_info[zone_name_class]["zone_soa_rdata"] = \
+            self._get_zone_soa(zone_name_class)
 
     def zonemgr_add_zone(self, zone_name_class):
         """ Add a zone into zone manager."""
-
-        logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_LOAD_ZONE, zone_name_class[0], zone_name_class[1])
+        logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_LOAD_ZONE, zone_name_class[0],
+                     zone_name_class[1])
         zone_info = {}
-        zone_soa = sqlite3_ds.get_zone_soa(str(zone_name_class[0]), self._db_file)
+        zone_soa = self._get_zone_soa(zone_name_class)
         if zone_soa is None:
             logger.warn(ZONEMGR_NO_SOA, zone_name_class[0], zone_name_class[1])
             zone_info["zone_soa_rdata"] = None
             zone_reload_time = 0.0
         else:
-            zone_info["zone_soa_rdata"] = zone_soa[7]
-            zone_reload_time = float(zone_soa[7].split(" ")[RETRY_OFFSET])
+            zone_info["zone_soa_rdata"] = zone_soa
+            zone_reload_time = float(zone_soa.split(" ")[RETRY_OFFSET])
         zone_info["zone_state"] = ZONE_OK
         zone_info["last_refresh_time"] = self._get_current_time()
         self._zonemgr_refresh_info[zone_name_class] = zone_info
-        # Imposes some random jitters to avoid many zones need to do refresh at the same time.
+        # Imposes some random jitters to avoid many zones need to do refresh
+        # at the same time.
         zone_reload_time = max(self._lowerbound_retry, zone_reload_time)
-        self._set_zone_timer(zone_name_class, zone_reload_time, self._reload_jitter * zone_reload_time)
+        self._set_zone_timer(zone_name_class, zone_reload_time,
+                             self._reload_jitter * zone_reload_time)
+
+    def _get_zone_soa(self, zone_name_class):
+        """Retrieve the current SOA RR of the zone to be transferred."""
+
+        def get_zone_soa_rrset(datasrc_client, zone_name, zone_class):
+            """Retrieve the current SOA RR of the zone to be transferred."""
+            # 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 in
+                # which this function is called, this shouldn't happen.
+                raise ZonemgrException(
+                    "unexpected result: zone %s/%s doesn't exist" %
+                    (zone_name.to_text(True), str(zone_class)))
+            result, soa_rrset, _ = finder.find(zone_name, RRType.SOA)
+            if result != ZoneFinder.SUCCESS:
+                logger.warn(ZONEMGR_NO_SOA,
+                            zone_name.to_text(True), str(zone_class))
+                return None
+            return soa_rrset
+
+        # Identify the data source to which the zone content is transferred,
+        # and get the current zone SOA from the data source (if available).
+        datasrc_client = None
+        clist = self._datasrc_clients_mgr.get_client_list(zone_name_class[1])
+        if clist is None:
+            return None
+        try:
+            datasrc_client = clist.find(zone_name_class[0], True, False)[0]
+            if datasrc_client is None: # can happen, so log it separately.
+                logger.error(ZONEMGR_DATASRC_UNKNOWN,
+                             zone_name_class[0] + '/' + zone_name_class[1])
+                return None
+            zone_soa = get_zone_soa_rrset(datasrc_client,
+                                          Name(zone_name_class[0]),
+                                          RRClass(zone_name_class[1]))
+            if zone_soa == None:
+                return None
+            else:
+                return zone_soa.get_rdata()[0].to_text()
+        except isc.datasrc.Error as ex:
+            # rare case error. re-raise as ZonemgrException so it'll be logged
+            # in command_handler().
+            raise ZonemgrException('unexpected failure in datasrc module: ' +
+                                   str(ex))
 
     def _zone_is_expired(self, zone_name_class):
         """Judge whether a zone is expired or not."""
-        zone_expired_time = float(self._get_zone_soa_rdata(zone_name_class).split(" ")[EXPIRED_OFFSET])
-        zone_last_refresh_time = self._get_zone_last_refresh_time(zone_name_class)
+        zone_expired_time = float(self._get_zone_soa_rdata(zone_name_class).
+                                      split(" ")[EXPIRED_OFFSET])
+        zone_last_refresh_time = \
+            self._get_zone_last_refresh_time(zone_name_class)
         if (ZONE_EXPIRED == self._get_zone_state(zone_name_class) or
-            zone_last_refresh_time + zone_expired_time <= self._get_current_time()):
+            zone_last_refresh_time + zone_expired_time <=
+                self._get_current_time()):
             return True
 
         return False
@@ -262,16 +324,19 @@ class ZonemgrRefresh:
         self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"] = time
 
     def _get_zone_notifier_master(self, zone_name_class):
-        if ("notify_master" in self._zonemgr_refresh_info[zone_name_class].keys()):
+        if ("notify_master" in
+                self._zonemgr_refresh_info[zone_name_class].keys()):
             return self._zonemgr_refresh_info[zone_name_class]["notify_master"]
 
         return None
 
     def _set_zone_notifier_master(self, zone_name_class, master_addr):
-        self._zonemgr_refresh_info[zone_name_class]["notify_master"] = master_addr
+        self._zonemgr_refresh_info[zone_name_class]["notify_master"] = \
+            master_addr
 
     def _clear_zone_notifier_master(self, zone_name_class):
-        if ("notify_master" in self._zonemgr_refresh_info[zone_name_class].keys()):
+        if ("notify_master" in
+                self._zonemgr_refresh_info[zone_name_class].keys()):
             del self._zonemgr_refresh_info[zone_name_class]["notify_master"]
 
     def _get_zone_state(self, zone_name_class):
@@ -295,7 +360,7 @@ class ZonemgrRefresh:
     def _send_command(self, module_name, command_name, params):
         """Send command between modules."""
         try:
-            self._mccs.rpc_call(command_name, module_name, params=params)
+            self._module_cc.rpc_call(command_name, module_name, params=params)
         except socket.error:
             # FIXME: WTF? Where does socket.error come from? And how do we ever
             # dare ignore such serious error? It can only be broken link to
@@ -314,7 +379,8 @@ class ZonemgrRefresh:
             # If hasn't received refresh response but are within refresh
             # timeout, skip the zone
             if (ZONE_REFRESHING == zone_state and
-                (self._get_zone_refresh_timeout(zone_name_class) > self._get_current_time())):
+                (self._get_zone_refresh_timeout(zone_name_class) >
+                     self._get_current_time())):
                 continue
 
             # Get the zone with minimum next_refresh_time
@@ -324,7 +390,8 @@ class ZonemgrRefresh:
                 zone_need_refresh = zone_name_class
 
             # Find the zone need do refresh
-            if (self._get_zone_next_refresh_time(zone_need_refresh) < self._get_current_time()):
+            if (self._get_zone_next_refresh_time(zone_need_refresh) <
+                    self._get_current_time()):
                 break
 
         return zone_need_refresh
@@ -332,9 +399,12 @@ class ZonemgrRefresh:
 
     def _do_refresh(self, zone_name_class):
         """Do zone refresh."""
-        logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_REFRESH_ZONE, zone_name_class[0], zone_name_class[1])
+        logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_REFRESH_ZONE,
+                     zone_name_class[0], zone_name_class[1])
         self._set_zone_state(zone_name_class, ZONE_REFRESHING)
-        self._set_zone_refresh_timeout(zone_name_class, self._get_current_time() + self._max_transfer_timeout)
+        self._set_zone_refresh_timeout(zone_name_class,
+                                       self._get_current_time() +
+                                           self._max_transfer_timeout)
         notify_master = self._get_zone_notifier_master(zone_name_class)
         # If the zone has notify master, send notify command to xfrin module
         if notify_master:
@@ -351,13 +421,6 @@ class ZonemgrRefresh:
                     }
             self._send_command(XFRIN_MODULE_NAME, ZONE_REFRESH_COMMAND, param)
 
-    def _zone_mgr_is_empty(self):
-        """Does zone manager has no zone?"""
-        if not len(self._zonemgr_refresh_info):
-            return True
-
-        return False
-
     def _run_timer(self, start_event):
         while self._running:
             # Notify run_timer that we already started and are inside the loop.
@@ -367,24 +430,29 @@ class ZonemgrRefresh:
             if start_event:
                 start_event.set()
                 start_event = None
-            # If zonemgr has no zone, set timer timeout to self._lowerbound_retry.
-            if self._zone_mgr_is_empty():
+            # If zonemgr has no zone, set timeout to minimum
+            if not self._zonemgr_refresh_info:
                 timeout = self._lowerbound_retry
             else:
                 zone_need_refresh = self._find_need_do_refresh_zone()
-                # If don't get zone with minimum next refresh time, set timer timeout to self._lowerbound_retry.
+                # If don't get zone with minimum next refresh time, set
+                # timeout to minimum
                 if not zone_need_refresh:
                     timeout = self._lowerbound_retry
                 else:
-                    timeout = self._get_zone_next_refresh_time(zone_need_refresh) - self._get_current_time()
-                    if (timeout < 0):
+                    timeout = \
+                        self._get_zone_next_refresh_time(zone_need_refresh) - \
+                        self._get_current_time()
+                    if timeout < 0:
                         self._do_refresh(zone_need_refresh)
                         continue
 
             """ Wait for the socket notification for a maximum time of timeout
             in seconds (as float)."""
             try:
-                rlist, wlist, xlist = select.select([self._check_sock, self._read_sock], [], [], timeout)
+                rlist, wlist, xlist = \
+                    select.select([self._check_sock, self._read_sock],
+                                  [], [], timeout)
             except select.error as e:
                 if e.args[0] == errno.EINTR:
                     (rlist, wlist, xlist) = ([], [], [])
@@ -403,8 +471,8 @@ class ZonemgrRefresh:
 
     def run_timer(self, daemon=False):
         """
-        Keep track of zone timers. Spawns and starts a thread. The thread object
-        is returned.
+        Keep track of zone timers. Spawns and starts a thread. The thread
+        object is returned.
 
         You can stop it by calling shutdown().
         """
@@ -429,6 +497,20 @@ class ZonemgrRefresh:
         # Return the thread to anyone interested
         return self._thread
 
+    def _datasrc_config_handler(self, new_config, config_data):
+        """Configuration handler of the 'data_sources' module.
+
+        The actual handling is delegated 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.
+        """
+        try:
+            self._datasrc_clients_mgr.reconfigure(new_config, config_data)
+        except isc.server_common.datasrc_clients_mgr.ConfigError as ex:
+            logger.error(ZONEMGR_DATASRC_CONFIG_ERROR, ex)
+
     def shutdown(self):
         """
         Stop the run_timer() thread. Block until it finished. This must be
@@ -450,7 +532,7 @@ class ZonemgrRefresh:
         self._read_sock = None
         self._write_sock = None
 
-    def update_config_data(self, new_config, module_cc_session):
+    def update_config_data(self, new_config, module_cc):
         """ update ZonemgrRefresh config """
         # Get a new value, but only if it is defined (commonly used below)
         # We don't use "value or default", because if value would be
@@ -499,7 +581,7 @@ class ZonemgrRefresh:
                     # Currently we use an explicit get_default_value call
                     # in case the class hasn't been set. Alternatively, we
                     # could use
-                    # module_cc_session.get_value('secondary_zones[INDEX]/class')
+                    # module_cc.get_value('secondary_zones[INDEX]/class')
                     # To get either the value that was set, or the default if
                     # it wasn't set.
                     # But the real solution would be to make new_config a type
@@ -509,7 +591,7 @@ class ZonemgrRefresh:
                     if 'class' in secondary_zone:
                         rr_class = secondary_zone['class']
                     else:
-                        rr_class = module_cc_session.get_default_value(
+                        rr_class = module_cc.get_default_value(
                                         'secondary_zones/class')
                     # Convert rr_class to and from RRClass to check its value
                     try:
@@ -521,10 +603,12 @@ class ZonemgrRefresh:
                     required[name_class] = True
                     # Add it only if it isn't there already
                     if not name_class in self._zonemgr_refresh_info:
-                        # If we are not able to find it in database, log an warning
+                        # If we are not able to find it in database, log an
+                        # warning
                         self.zonemgr_add_zone(name_class)
                 # Drop the zones that are no longer there
-                # Do it in two phases, python doesn't like deleting while iterating
+                # Do it in two phases, python doesn't like deleting while
+                # iterating
                 to_drop = []
                 for old_zone in self._zonemgr_refresh_info:
                     if not old_zone in required:
@@ -539,10 +623,11 @@ class Zonemgr:
     def __init__(self):
         self._zone_refresh = None
         self._setup_session()
-        self._db_file = self.get_db_file()
-        # Create socket pair for communicating between main thread and zonemgr timer thread
-        self._master_socket, self._slave_socket = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
-        self._zone_refresh = ZonemgrRefresh(self._db_file, self._slave_socket, self._module_cc)
+        # Create socket pair for communicating between main thread and zonemgr
+        # timer thread
+        self._master_socket, self._slave_socket = \
+            socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
+        self._zone_refresh = ZonemgrRefresh(self._slave_socket, self._module_cc)
         self._zone_refresh.run_timer()
 
         self._lock = threading.Lock()
@@ -550,9 +635,10 @@ class Zonemgr:
         self.running = False
 
     def _setup_session(self):
-        """Setup two sessions for zonemgr, one(self._module_cc) is used for receiving
-        commands and config data sent from other modules, another one (self._cc)
-        is used to send commands to proper modules."""
+        """Setup two sessions for zonemgr, one(self._module_cc) is used for
+        receiving commands and config data sent from other modules, another
+        one (self._cc) is used to send commands to proper modules.
+        """
         self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
                                                   self.config_handler,
                                                   self.command_handler)
@@ -561,18 +647,9 @@ class Zonemgr:
         self._config_data_check(self._config_data)
         self._module_cc.start()
 
-    def get_db_file(self):
-        db_file, is_default = self._module_cc.get_remote_config_value(AUTH_MODULE_NAME, "database_file")
-        # this too should be unnecessary, but currently the
-        # 'from build' override isn't stored in the config
-        # (and we don't have indirect python access to datasources yet)
-        if is_default and "B10_FROM_BUILD" in os.environ:
-            db_file = os.environ["B10_FROM_BUILD"] + "/bind10_zones.sqlite3"
-        return db_file
-
     def shutdown(self):
         """Shutdown the zonemgr process. The thread which is keeping track of
-           zone timers should be terminated.
+        zone timers should be terminated.
         """
         self._zone_refresh.shutdown()
 
@@ -596,7 +673,8 @@ class Zonemgr:
         self._config_data_check(complete)
         if self._zone_refresh is not None:
             try:
-                self._zone_refresh.update_config_data(complete, self._module_cc)
+                self._zone_refresh.update_config_data(complete,
+                                                      self._module_cc)
             except Exception as e:
                 answer = create_answer(1, str(e))
                 ok = False
@@ -608,7 +686,8 @@ class Zonemgr:
     def _config_data_check(self, config_data):
         """Check whether the new config data is valid or
         not. It contains only basic logic, not full check against
-        database."""
+        database.
+        """
         # jitter should not be bigger than half of the original value
         if config_data.get('refresh_jitter') > 0.5:
             config_data['refresh_jitter'] = 0.5
@@ -625,7 +704,7 @@ class Zonemgr:
             logger.error(ZONEMGR_NO_ZONE_CLASS)
             raise ZonemgrException("zone class should be provided")
 
-        if (command != ZONE_NOTIFY_COMMAND):
+        if command != ZONE_NOTIFY_COMMAND:
             return (zone_name, zone_class)
 
         master_str = args.get("master")
@@ -641,7 +720,8 @@ class Zonemgr:
         ZONE_NOTIFY_COMMAND is issued by Auth process;
         ZONE_NEW_DATA_READY_CMD and ZONE_XFRIN_FAILED are issued by
         Xfrin process;
-        shutdown is issued by a user or Init process. """
+        shutdown is issued by a user or Init process.
+        """
         answer = create_answer(0)
         if command == ZONE_NOTIFY_COMMAND:
             """ Handle Auth notify command"""

+ 11 - 1
src/bin/zonemgr/zonemgr_messages.mes

@@ -1,4 +1,4 @@
-# Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+# Copyright (C) 2011-2013  Internet Systems Consortium, Inc. ("ISC")
 #
 # Permission to use, copy, modify, and/or distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
@@ -19,6 +19,16 @@
 An error was encountered on the command channel.  The message indicates
 the nature of the error.
 
+% ZONEMGR_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 zonemgr.  The zonemgr 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 zonemgr
+rejects the new configuration then the entire BIND 10 system will have
+inconsistent state among different modules.  If other modules accept
+the update but zonemgr produces this error, the zonemgr module should
+probably be restarted.
+
 % ZONEMGR_JITTER_TOO_BIG refresh_jitter is too big, setting to 0.5
 The value specified in the configuration for the refresh jitter is too large
 so its value has been set to the maximum of 0.5.

+ 0 - 1
tests/lettuce/configurations/xfrin/retransfer_slave.conf.orig

@@ -8,7 +8,6 @@
         } ]
     },
     "Auth": {
-        "database_file": "data/test_nonexistent_db.sqlite3",
         "listen_on": [ {
             "address": "::1",
             "port": 47806

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

@@ -8,7 +8,6 @@
         } ]
     },
     "Auth": {
-        "database_file": "data/xfrin-before-diffs.sqlite3",
         "listen_on": [ {
             "address": "::1",
             "port": 47806

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

@@ -8,7 +8,6 @@
         } ]
     },
     "Auth": {
-        "database_file": "data/xfrin-notify.sqlite3",
         "listen_on": [ {
             "address": "::1",
             "port": 47806

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

@@ -8,7 +8,6 @@
         } ]
     },
     "Auth": {
-        "database_file": "data/xfrin-notify.sqlite3",
         "listen_on": [ {
             "address": "127.0.0.1",
             "port": 47806