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"
 SPECFILE_LOCATION = SPECFILE_PATH + "/ddns.spec"
 SOCKET_FILE = SOCKET_FILE_PATH + '/ddns_socket'
 SOCKET_FILE = SOCKET_FILE_PATH + '/ddns_socket'
 AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + '/auth.spec'
 AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + '/auth.spec'
+ZONEMGR_MODULE_NAME = 'Zonemgr'
 
 
 isc.util.process.rename()
 isc.util.process.rename()
 
 
@@ -181,8 +182,13 @@ class DDNSServer:
             self._cc.get_default_value('zones'))
             self._cc.get_default_value('zones'))
         self._cc.start()
         self._cc.start()
 
 
+        # A list of secondary zones, retrieved from zonemgr configuration.
+        self._secondary_zones = None
+
         # Get necessary configurations from remote modules.
         # Get necessary configurations from remote modules.
         self._cc.add_remote_config(AUTH_SPECFILE_LOCATION)
         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)
         isc.server_common.tsig_keyring.init_keyring(self._cc)
 
 
         self._shutdown = False
         self._shutdown = False
@@ -256,6 +262,49 @@ class DDNSServer:
             answer = create_answer(1, "Unknown command: " + str(cmd))
             answer = create_answer(1, "Unknown command: " + str(cmd))
         return answer
         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):
     def trigger_shutdown(self):
         '''Initiate a shutdown sequence.
         '''Initiate a shutdown sequence.
 
 
@@ -367,8 +416,8 @@ class DDNSServer:
         # ZoneConfig will soon be substantially revised.  For now we don't
         # ZoneConfig will soon be substantially revised.  For now we don't
         # bother to generalize it.
         # bother to generalize it.
         datasrc_class, datasrc_client = get_datasrc_client(self._cc)
         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,
         update_session = self._UpdateSessionClass(self.__request_msg,
                                                   remote_addr, zone_cfg)
                                                   remote_addr, zone_cfg)
         result, zname, zclass = update_session.handle()
         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
 either case, this notification is generally expected to succeed; so
 the fact it fails itself means there's something wrong in the BIND 10
 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.
 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
 import isc.util.cio.socketsession
 from isc.cc.session import SessionTimeout, SessionError, ProtocolError
 from isc.cc.session import SessionTimeout, SessionError, ProtocolError
 from isc.datasrc import DataSourceClient
 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.config.ccsession import create_answer
 from isc.server_common.dns_tcp import DNSTCPContext
 from isc.server_common.dns_tcp import DNSTCPContext
 import ddns
 import ddns
@@ -56,6 +58,11 @@ TEST_TSIG_KEYRING.add(TEST_TSIG_KEY)
 # Another TSIG key not in the keyring, making verification fail
 # Another TSIG key not in the keyring, making verification fail
 BAD_TSIG_KEY = TSIGKey("example.com:SFuWd/q99SzF8Yzd1QbB9g==")
 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:
 class FakeSocket:
     """
     """
     A fake socket. It only provides a file number, peer name and accept method.
     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._sendmsg_exception = None # will be raised from sendmsg if !None
         self._recvmsg_exception = None # will be raised from recvmsg 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):
     def start(self):
         '''Called by DDNSServer initialization, but not used in tests'''
         '''Called by DDNSServer initialization, but not used in tests'''
         self._started = True
         self._started = True
@@ -222,9 +233,17 @@ class MyCCSession(isc.config.ConfigData):
         """
         """
         return FakeSocket(1)
         return FakeSocket(1)
 
 
-    def add_remote_config(self, spec_file_name):
+    def add_remote_config(self, spec_file_name, update_callback=None):
         pass
         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):
     def get_remote_config_value(self, module_name, item):
         if module_name == "Auth" and item == "database_file":
         if module_name == "Auth" and item == "database_file":
             return self.auth_db_file, False
             return self.auth_db_file, False
@@ -233,6 +252,14 @@ class MyCCSession(isc.config.ConfigData):
                 return [], True # default
                 return [], True # default
             else:
             else:
                 return self.auth_datasources, False
                 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):
     def group_sendmsg(self, msg, group):
         # remember the passed parameter, and return dummy sequence
         # 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)]
         acl = self.ddns_server._zone_config[(TEST_ZONE_NAME, TEST_RRCLASS)]
         self.assertEqual(ACCEPT, acl.execute(TEST_ACL_CONTEXT))
         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):
     def test_shutdown_command(self):
         '''Test whether the shutdown command works'''
         '''Test whether the shutdown command works'''
         self.assertFalse(self.ddns_server._shutdown)
         self.assertFalse(self.ddns_server._shutdown)