diff --git a/src/bin/xfrin/tests/xfrin_test.py b/src/bin/xfrin/tests/xfrin_test.py index a5c92ab01f..b88d6a99b9 100644 --- a/src/bin/xfrin/tests/xfrin_test.py +++ b/src/bin/xfrin/tests/xfrin_test.py @@ -139,6 +139,9 @@ class MockCC(MockModuleCCSession): if identifier == "zones/use_ixfr": return False + def remove_remote_config(self, module_name): + pass + class MockDataSourceClient(): '''A simple mock data source client. @@ -2574,6 +2577,134 @@ class TestXfrin(unittest.TestCase): self.common_ixfr_setup('refresh', False) 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(): raise KeyboardInterrupt() diff --git a/src/bin/xfrin/xfrin.py.in b/src/bin/xfrin/xfrin.py.in index 863c5b9234..a60ad7f707 100755 --- a/src/bin/xfrin/xfrin.py.in +++ b/src/bin/xfrin/xfrin.py.in @@ -1246,6 +1246,11 @@ 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() @@ -1264,6 +1269,8 @@ class Xfrin: 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) def _cc_check_command(self): '''This is a straightforward wrapper for cc.check_command, @@ -1310,10 +1317,78 @@ 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) + """ + # 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): ''' 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() @@ -1446,20 +1521,19 @@ class Xfrin: return (addr.family, socket.SOCK_STREAM, (str(addr), port)) 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: - # 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): '''Send command to xfrout/zone manager module.