Browse Source

[1165] added new configuration data: "zone_config". While the name is
generic, the intent is to use it for transfer ACL per zone. The notion of
per-zone configuration should eventually be implemented in a more generic
way (not specific to xfrout).

JINMEI Tatuya 13 years ago
parent
commit
8cc8f4c008
2 changed files with 111 additions and 9 deletions
  1. 70 5
      src/bin/xfrout/tests/xfrout_test.py.in
  2. 41 4
      src/bin/xfrout/xfrout.py.in

+ 70 - 5
src/bin/xfrout/tests/xfrout_test.py.in

@@ -636,17 +636,17 @@ class TestUnixSockServer(unittest.TestCase):
                                              socket.AI_NUMERICHOST)[0][4])
         self.assertEqual(isc.acl.acl.ACCEPT, self.unix._acl.execute(context))
 
-    def check_loaded_ACL(self):
+    def check_loaded_ACL(self, acl):
         context = isc.acl.dns.RequestContext(socket.getaddrinfo("127.0.0.1",
                                              1234, 0, socket.SOCK_DGRAM,
                                              socket.IPPROTO_UDP,
                                              socket.AI_NUMERICHOST)[0][4])
-        self.assertEqual(isc.acl.acl.ACCEPT, self.unix._acl.execute(context))
+        self.assertEqual(isc.acl.acl.ACCEPT, acl.execute(context))
         context = isc.acl.dns.RequestContext(socket.getaddrinfo("192.0.2.1",
                                              1234, 0, socket.SOCK_DGRAM,
                                              socket.IPPROTO_UDP,
                                              socket.AI_NUMERICHOST)[0][4])
-        self.assertEqual(isc.acl.acl.REJECT, self.unix._acl.execute(context))
+        self.assertEqual(isc.acl.acl.REJECT, acl.execute(context))
 
     def test_update_config_data(self):
         self.check_default_ACL()
@@ -673,12 +673,77 @@ class TestUnixSockServer(unittest.TestCase):
         # Load the ACL
         self.unix.update_config_data({'query_acl': [{'from': '127.0.0.1',
                                                'action': 'ACCEPT'}]})
-        self.check_loaded_ACL()
+        self.check_loaded_ACL(self.unix._acl)
         # Pass a wrong data there and check it does not replace the old one
         self.assertRaises(isc.acl.acl.LoaderError,
                           self.unix.update_config_data,
                           {'query_acl': ['Something bad']})
-        self.check_loaded_ACL()
+        self.check_loaded_ACL(self.unix._acl)
+
+    def test_zone_config_data(self):
+        # By default, there's no specific zone config
+        self.assertEqual({}, self.unix._zone_config)
+
+        # Adding config for a specific zone.  The config is empty unless
+        # explicitly specified.
+        self.unix.update_config_data({'zone_config':
+                                          [{'origin': 'example.com',
+                                            'class': 'IN'}]})
+        self.assertEqual({}, self.unix._zone_config[('IN', 'example.com.')])
+
+        # zone class can be omitted
+        self.unix.update_config_data({'zone_config':
+                                          [{'origin': 'example.com'}]})
+        self.assertEqual({}, self.unix._zone_config[('IN', 'example.com.')])
+
+        # zone class, name are stored in the "normalized" form.  class
+        # strings are upper cased, names are down cased.
+        self.unix.update_config_data({'zone_config':
+                                          [{'origin': 'EXAMPLE.com'}]})
+        self.assertEqual({}, self.unix._zone_config[('IN', 'example.com.')])
+
+        # invalid zone class, name will result in exceptions
+        self.assertRaises(EmptyLabel,
+                          self.unix.update_config_data,
+                          {'zone_config': [{'origin': 'bad..example'}]})
+        self.assertRaises(InvalidRRClass,
+                          self.unix.update_config_data,
+                          {'zone_config': [{'origin': 'example.com',
+                                            'class': 'badclass'}]})
+
+        # Configuring a couple of more zones
+        self.unix.update_config_data({'zone_config':
+                                          [{'origin': 'example.com'},
+                                           {'origin': 'example.com',
+                                            'class': 'CH'},
+                                           {'origin': 'example.org'}]})
+        self.assertEqual({}, self.unix._zone_config[('IN', 'example.com.')])
+        self.assertEqual({}, self.unix._zone_config[('CH', 'example.com.')])
+        self.assertEqual({}, self.unix._zone_config[('IN', 'example.org.')])
+
+        # Duplicate data: should be rejected with an exception
+        self.assertRaises(ValueError,
+                          self.unix.update_config_data,
+                          {'zone_config': [{'origin': 'example.com'},
+                                           {'origin': 'example.org'},
+                                           {'origin': 'example.com'}]})
+
+    def test_zone_config_data_with_acl(self):
+        # Similar to the previous test, but with transfer_acl config
+        self.unix.update_config_data({'zone_config':
+                                          [{'origin': 'example.com',
+                                            'transfer_acl':
+                                                [{'from': '127.0.0.1',
+                                                  'action': 'ACCEPT'}]}]})
+        acl = self.unix._zone_config[('IN', 'example.com.')]['transfer_acl']
+        self.check_loaded_ACL(acl)
+
+        # invalid ACL syntax will be rejected with exception
+        self.assertRaises(isc.acl.acl.LoaderError,
+                          self.unix.update_config_data,
+                          {'zone_config': [{'origin': 'example.com',
+                                            'transfer_acl':
+                                                [{'action': 'BADACTION'}]}]})
 
     def test_get_db_file(self):
         self.assertEqual(self.unix.get_db_file(), "initdb.file")

+ 41 - 4
src/bin/xfrout/xfrout.py.in

@@ -86,6 +86,9 @@ TSIG_SIGN_EVERY_NTH = 96
 
 XFROUT_MAX_MESSAGE_SIZE = 65535
 
+# In practice, RR class is almost always fixed, so if and when we allow
+# it to be configured, it's convenient to make it optional.
+DEFAULT_RRCLASS = RRClass('IN')
 
 def get_rrset_len(rrset):
     """Returns the wire length of the given RRset"""
@@ -401,10 +404,11 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn, ThreadingUnixStreamServer):
     def _common_init(self):
         self._lock = threading.Lock()
         self._transfers_counter = 0
-        # This default value will probably get overwritten by the (same)
-        # default value from the spec file. This is here just to make
-        # sure and to make the default value in tests consistent.
+        # These default values will probably get overwritten by the (same)
+        # default value from the spec file. These are here just to make
+        # sure and to make the default values in tests consistent.
         self._acl = REQUEST_LOADER.load('[{"action": "ACCEPT"}]')
+        self._zone_config = {}
 
     def _receive_query_message(self, sock):
         ''' receive request message from sock'''
@@ -551,16 +555,49 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn, ThreadingUnixStreamServer):
             pass
 
     def update_config_data(self, new_config):
-        '''Apply the new config setting of xfrout module. '''
+        '''Apply the new config setting of xfrout module.
+
+        Note: this method does not provide strong exception guarantee;
+        if an exception is raised in the middle of parsing and building the
+        given config data, the incomplete set of new configuration will
+        remain.  This should be fixed.
+        '''
         logger.info(XFROUT_NEW_CONFIG)
         if 'query_acl' in new_config:
             self._acl = REQUEST_LOADER.load(new_config['query_acl'])
+        if 'zone_config' in new_config:
+            self._zone_config = \
+                self.__create_zone_config(new_config.get('zone_config'))
         self._lock.acquire()
         self._max_transfers_out = new_config.get('transfers_out')
         self.set_tsig_key_ring(new_config.get('tsig_key_ring'))
         self._lock.release()
         logger.info(XFROUT_NEW_CONFIG_DONE)
 
+    def __create_zone_config(self, zone_config_list):
+        new_config = {}
+        for zconf in zone_config_list:
+            # convert the class, origin (name) pair.  First build pydnspp
+            # object to reject invalid input.
+            if 'class' in zconf:
+                zclass = RRClass(zconf['class'])
+            else:
+                zclass = DEFAULT_RRCLASS
+            zorigin = Name(zconf['origin'], True)
+            config_key = (zclass.to_text(), zorigin.to_text())
+
+            # reject duplicate config
+            if config_key in new_config:
+                raise ValueError('Duplicaet zone_config for ' +
+                                 str(zorigin) + '/' + str(zclass))
+
+            # create a new config entry, build any given (and known) config
+            new_config[config_key] = {}
+            if 'transfer_acl' in zconf:
+                new_config[config_key]['transfer_acl'] = \
+                    REQUEST_LOADER.load(zconf['transfer_acl'])
+        return new_config
+
     def set_tsig_key_ring(self, key_list):
         """Set the tsig_key_ring , given a TSIG key string list representation. """