Browse Source

[2020] get/maintain 2ndary zones from zonemgr and use it for update sessions.

JINMEI Tatuya 13 years ago
parent
commit
3a21a6c5b9
3 changed files with 137 additions and 3 deletions
  1. 51 2
      src/bin/ddns/ddns.py.in
  2. 24 0
      src/bin/ddns/ddns_messages.mes
  3. 62 1
      src/bin/ddns/tests/ddns_test.py

+ 51 - 2
src/bin/ddns/ddns.py.in

@@ -79,6 +79,7 @@ else:
 SPECFILE_LOCATION = SPECFILE_PATH + "/ddns.spec"
 SOCKET_FILE = SOCKET_FILE_PATH + '/ddns_socket'
 AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + '/auth.spec'
+ZONEMGR_MODULE_NAME = 'Zonemgr'
 
 isc.util.process.rename()
 
@@ -181,8 +182,13 @@ class DDNSServer:
             self._cc.get_default_value('zones'))
         self._cc.start()
 
+        # A list of secondary zones, retrieved from zonemgr configuration.
+        self._secondary_zones = None
+
         # Get necessary configurations from remote modules.
         self._cc.add_remote_config(AUTH_SPECFILE_LOCATION)
+        self._cc.add_remote_config_by_name(ZONEMGR_MODULE_NAME,
+                                           self.__zonemgr_config_handler)
         isc.server_common.tsig_keyring.init_keyring(self._cc)
 
         self._shutdown = False
@@ -256,6 +262,49 @@ class DDNSServer:
             answer = create_answer(1, "Unknown command: " + str(cmd))
         return answer
 
+    def __zonemgr_config_handler(self, new_config, module_config):
+        logger.info(DDNS_RECEIVED_ZONEMGR_UPDATE)
+
+        # If we've got the config before and the new config doesn't update
+        # the secondary zone list, there's nothing we should do with it.
+        # Note: there seems to be a bug either in bindctl or cfgmgr, and
+        # new_config can contain 'secondary_zones' even if it's not really
+        # updated.  We still perform the check so we can avoid redundant
+        # resetting when the bug is fixed.  The redundant reset itself is not
+        # good, but such configuration update should not happen so often and
+        # it should be acceptable in practice.
+        if self._secondary_zones is not None and \
+                not 'secondary_zones' in new_config:
+            return
+
+        # Get the latest secondary zones.  Use get_remote_config_value() so
+        # it can work for both the initial default case and updates.
+        sec_zones, _ = self._cc.get_remote_config_value(ZONEMGR_MODULE_NAME,
+                                                        'secondary_zones')
+        new_secondary_zones = []
+        try:
+            # Parse the new config and build a new list of secondary zones.
+            # Note that validation should have been done by zonemgr, so
+            # the following shouldn't fail in theory.  But the configuration
+            # interface is quite complicated and there may be a hole, so
+            # we'll perform minimal defense ourselves.
+            for zone_spec in sec_zones:
+                zname = Name(zone_spec['name'])
+                # class is optional per spec.  ideally this should be merged
+                # within the config module, but it's not really clear if we
+                # can assume that due to its complexity - so we don't rely on
+                # it.
+                if 'class' in zone_spec:
+                    zclass = RRClass(zone_spec['class'])
+                else:
+                    zclass = RRClass(module_config.get_default_value(
+                            'secondary_zones/class'))
+                new_secondary_zones.append((zname, zclass))
+            self._secondary_zones = new_secondary_zones
+            logger.info(DDNS_SECONDARY_ZONES_UPDATE, len(self._secondary_zones))
+        except Exception as ex:
+            logger.error(DDNS_SECONDARY_ZONES_UPDATE_FAIL, ex)
+
     def trigger_shutdown(self):
         '''Initiate a shutdown sequence.
 
@@ -367,8 +416,8 @@ class DDNSServer:
         # ZoneConfig will soon be substantially revised.  For now we don't
         # bother to generalize it.
         datasrc_class, datasrc_client = get_datasrc_client(self._cc)
-        zone_cfg = ZoneConfig([], datasrc_class, datasrc_client,
-                              self._zone_config)
+        zone_cfg = ZoneConfig(self._secondary_zones, datasrc_class,
+                              datasrc_client, self._zone_config)
         update_session = self._UpdateSessionClass(self.__request_msg,
                                                   remote_addr, zone_cfg)
         result, zname, zclass = update_session.handle()

+ 24 - 0
src/bin/ddns/ddns_messages.mes

@@ -162,3 +162,27 @@ needs to examine this message and takes an appropriate action.  In
 either case, this notification is generally expected to succeed; so
 the fact it fails itself means there's something wrong in the BIND 10
 system, and it would be advisable to check other log messages.
+
+% DDNS_SECONDARY_ZONES_UPDATE_FAIL failed to update secondary zone list: %1
+An error message.  b10-ddns was notified of updates to a list of
+secondary zones from b10-zonemgr and tried to update its own internal
+copy of the list, but it failed.  This can happen only if the
+configuration contains an error, but such a configuration should have
+been rejected by b10-zonemgr first and shouldn't be delivered to
+b10-ddns, so this should basically be an internal bug.  It's advisable
+to submit a bug report if you ever see this message.  Also, while
+b10-ddns still keeps running with the previous configuration when this
+error happens, it's possible that the entire system is in an
+inconsistent state.  So it's probably better to restart bind10, or at
+least restart b10-ddns.
+
+% DDNS_RECEIVED_ZONEMGR_UPDATE received configuration updates from zonemgr
+b10-ddns is notified of updates to b10-zonemgr's configuration
+(including a report of the initial configuration).  It may possibly
+contain changes to the secondary zones, in which case b10-ddns will
+update its internal copy of that configuration.
+
+% DDNS_SECONDARY_ZONES_UPDATE updated secondary zone list (%1 zones are listed)
+b10-ddns has successfully updated the internal copy of secondary zones
+obtained from b10-zonemgr, based on a latest update to zonemgr's
+configuration.  The number of newly configured secondary zones is logged.

+ 62 - 1
src/bin/ddns/tests/ddns_test.py

@@ -21,6 +21,8 @@ from isc.acl.acl import ACCEPT
 import isc.util.cio.socketsession
 from isc.cc.session import SessionTimeout, SessionError, ProtocolError
 from isc.datasrc import DataSourceClient
+from isc.config import module_spec_from_file
+from isc.config.config_data import ConfigData
 from isc.config.ccsession import create_answer
 from isc.server_common.dns_tcp import DNSTCPContext
 import ddns
@@ -56,6 +58,11 @@ TEST_TSIG_KEYRING.add(TEST_TSIG_KEY)
 # Another TSIG key not in the keyring, making verification fail
 BAD_TSIG_KEY = TSIGKey("example.com:SFuWd/q99SzF8Yzd1QbB9g==")
 
+# Incorporate it so we can use the real default values of zonemgr config
+# in the tests.
+ZONEMGR_MODULE_SPEC = module_spec_from_file(
+    os.environ["B10_FROM_BUILD"] + "/src/bin/zonemgr/zonemgr.spec")
+
 class FakeSocket:
     """
     A fake socket. It only provides a file number, peer name and accept method.
@@ -208,6 +215,10 @@ class MyCCSession(isc.config.ConfigData):
         self._sendmsg_exception = None # will be raised from sendmsg if !None
         self._recvmsg_exception = None # will be raised from recvmsg if !None
 
+        # Attributes to handle (faked) remote configurations
+        self.__callbacks = {}   # record callbacks for updates to remote confs
+        self._zonemgr_config = {} # faked zonemgr cfg, settable by tests
+
     def start(self):
         '''Called by DDNSServer initialization, but not used in tests'''
         self._started = True
@@ -222,9 +233,17 @@ class MyCCSession(isc.config.ConfigData):
         """
         return FakeSocket(1)
 
-    def add_remote_config(self, spec_file_name):
+    def add_remote_config(self, spec_file_name, update_callback=None):
         pass
 
+    def add_remote_config_by_name(self, module_name, update_callback=None):
+        if update_callback is not None:
+            self.__callbacks[module_name] = update_callback
+        if module_name is 'Zonemgr':
+            if module_name in self.__callbacks:
+                self.__callbacks[module_name](self._zonemgr_config,
+                                              ConfigData(ZONEMGR_MODULE_SPEC))
+
     def get_remote_config_value(self, module_name, item):
         if module_name == "Auth" and item == "database_file":
             return self.auth_db_file, False
@@ -233,6 +252,14 @@ class MyCCSession(isc.config.ConfigData):
                 return [], True # default
             else:
                 return self.auth_datasources, False
+        if module_name == 'Zonemgr' and item == 'secondary_zones':
+            if item in self._zonemgr_config:
+                return self._zonemgr_config[item], False
+            else:
+                seczone_default = \
+                    ConfigData(ZONEMGR_MODULE_SPEC).get_default_value(
+                    'secondary_zones')
+                return seczone_default, True
 
     def group_sendmsg(self, msg, group):
         # remember the passed parameter, and return dummy sequence
@@ -422,6 +449,40 @@ class TestDDNSServer(unittest.TestCase):
         acl = self.ddns_server._zone_config[(TEST_ZONE_NAME, TEST_RRCLASS)]
         self.assertEqual(ACCEPT, acl.execute(TEST_ACL_CONTEXT))
 
+    def test_secondary_zones_config(self):
+        # By default it should be an empty list
+        self.assertEqual([], self.ddns_server._secondary_zones)
+
+        # emulating an update.  calling add_remote_config_by_name is a
+        # convenient faked way to invoke the callback.
+        self.__cc_session._zonemgr_config = {'secondary_zones': [
+                {'name': TEST_ZONE_NAME_STR, 'class': TEST_RRCLASS_STR}]}
+        self.__cc_session.add_remote_config_by_name('Zonemgr')
+
+        # The new set of secondary zones should be stored.
+        self.assertEqual([(TEST_ZONE_NAME, TEST_RRCLASS)],
+                         self.ddns_server._secondary_zones)
+
+        # Similar to the above, but the optional 'class' is missing.
+        self.__cc_session._zonemgr_config = {'secondary_zones': [
+                {'name': TEST_ZONE_NAME_STR}]}
+        self.__cc_session.add_remote_config_by_name('Zonemgr')
+        self.assertEqual([(TEST_ZONE_NAME, TEST_RRCLASS)],
+                         self.ddns_server._secondary_zones)
+
+        # Check the 2ndary zones aren't changed if the new config doesn't
+        # update it.
+        seczones_orig = self.ddns_server._secondary_zones
+        self.ddns_server._secondary_zones = 42 # dummy value, should be kept.
+        self.__cc_session._zonemgr_config = {}
+        self.__cc_session.add_remote_config_by_name('Zonemgr')
+        self.assertEqual(42, self.ddns_server._secondary_zones)
+        self.ddns_server._secondary_zones = seczones_orig
+
+        self.__cc_session._zonemgr_config = {'secondary_zones': [
+                {'name': 'badd..example', 'class': TEST_RRCLASS_STR}]}
+        self.__cc_session.add_remote_config_by_name('Zonemgr')
+
     def test_shutdown_command(self):
         '''Test whether the shutdown command works'''
         self.assertFalse(self.ddns_server._shutdown)