Browse Source

[master] Merge branch 'trac1789'

Jelte Jansen 13 years ago
parent
commit
5dcf1dadd1
2 changed files with 218 additions and 13 deletions
  1. 131 0
      src/bin/xfrin/tests/xfrin_test.py
  2. 87 13
      src/bin/xfrin/xfrin.py.in

+ 131 - 0
src/bin/xfrin/tests/xfrin_test.py

@@ -139,6 +139,9 @@ class MockCC(MockModuleCCSession):
         if identifier == "zones/use_ixfr":
         if identifier == "zones/use_ixfr":
             return False
             return False
 
 
+    def remove_remote_config(self, module_name):
+        pass
+
 class MockDataSourceClient():
 class MockDataSourceClient():
     '''A simple mock data source client.
     '''A simple mock data source client.
 
 
@@ -2574,6 +2577,134 @@ class TestXfrin(unittest.TestCase):
         self.common_ixfr_setup('refresh', False)
         self.common_ixfr_setup('refresh', False)
         self.assertEqual(RRType.AXFR(), self.xfr.xfrin_started_request_type)
         self.assertEqual(RRType.AXFR(), self.xfr.xfrin_started_request_type)
 
 
+class TextXfrinMemoryZones(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 raise_interrupt():
 def raise_interrupt():
     raise KeyboardInterrupt()
     raise KeyboardInterrupt()
 
 

+ 87 - 13
src/bin/xfrin/xfrin.py.in

@@ -1246,6 +1246,11 @@ class Xfrin:
     def __init__(self):
     def __init__(self):
         self._max_transfers_in = 10
         self._max_transfers_in = 10
         self._zones = {}
         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._cc_setup()
         self.recorder = XfrinRecorder()
         self.recorder = XfrinRecorder()
         self._shutdown_event = threading.Event()
         self._shutdown_event = threading.Event()
@@ -1264,6 +1269,8 @@ class Xfrin:
         self._module_cc.start()
         self._module_cc.start()
         config_data = self._module_cc.get_full_config()
         config_data = self._module_cc.get_full_config()
         self.config_handler(config_data)
         self.config_handler(config_data)
+        self._module_cc.add_remote_config(AUTH_SPECFILE_LOCATION,
+                                          self._auth_config_handler)
 
 
     def _cc_check_command(self):
     def _cc_check_command(self):
         '''This is a straightforward wrapper for cc.check_command,
         '''This is a straightforward wrapper for cc.check_command,
@@ -1310,10 +1317,78 @@ class Xfrin:
 
 
         return create_answer(0)
         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)
+        """
+        # 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
+
     def shutdown(self):
     def shutdown(self):
         ''' shutdown the xfrin process. the thread which is doing xfrin should be
         ''' shutdown the xfrin process. the thread which is doing xfrin should be
         terminated.
         terminated.
         '''
         '''
+        self._module_cc.remove_remote_config(AUTH_SPECFILE_LOCATION)
         self._module_cc.send_stopping()
         self._module_cc.send_stopping()
         self._shutdown_event.set()
         self._shutdown_event.set()
         main_thread = threading.currentThread()
         main_thread = threading.currentThread()
@@ -1446,20 +1521,19 @@ class Xfrin:
         return (addr.family, socket.SOCK_STREAM, (str(addr), port))
         return (addr.family, socket.SOCK_STREAM, (str(addr), port))
 
 
     def _get_db_file(self):
     def _get_db_file(self):
-        #TODO, the db file path should be got in auth server's configuration
-        # if we need access to this configuration more often, we
-        # should add it on start, and not remove it here
-        # (or, if we have writable ds, we might not need this in
-        # the first place)
-        self._module_cc.add_remote_config(AUTH_SPECFILE_LOCATION)
-        db_file, is_default = self._module_cc.get_remote_config_value("Auth", "database_file")
+        return self._db_file
+
+    def _set_db_file(self):
+        db_file, is_default =\
+            self._module_cc.get_remote_config_value("Auth", "database_file")
         if is_default and "B10_FROM_BUILD" in os.environ:
         if is_default and "B10_FROM_BUILD" in os.environ:
-            # this too should be unnecessary, but currently the
-            # 'from build' override isn't stored in the config
-            # (and we don't have writable datasources yet)
-            db_file = os.environ["B10_FROM_BUILD"] + os.sep + "bind10_zones.sqlite3"
-        self._module_cc.remove_remote_config(AUTH_SPECFILE_LOCATION)
-        return db_file
+            # 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):
     def publish_xfrin_news(self, zone_name, zone_class,  xfr_result):
         '''Send command to xfrout/zone manager module.
         '''Send command to xfrout/zone manager module.