Michal 'vorner' Vaner 13 years ago
parent
commit
5a7953933a

+ 2 - 0
configure.ac

@@ -1005,6 +1005,8 @@ AC_CONFIG_FILES([Makefile
                  src/lib/python/isc/bind10/tests/Makefile
                  src/lib/python/isc/xfrin/Makefile
                  src/lib/python/isc/xfrin/tests/Makefile
+                 src/lib/python/isc/server_common/Makefile
+                 src/lib/python/isc/server_common/tests/Makefile
                  src/lib/config/Makefile
                  src/lib/config/tests/Makefile
                  src/lib/config/tests/testdata/Makefile

+ 5 - 13
doc/guide/bind10-guide.xml

@@ -1629,31 +1629,23 @@ Xfrout/transfer_acl[0]	{"action": "ACCEPT"}	any	(default)</screen>
     </simpara></note>
 
     <para>
-      If you want to require TSIG in access control, a separate TSIG
-      "key ring" must be configured specifically
-      for <command>b10-xfrout</command> as well as a system wide
-      key ring, both containing a consistent set of keys.
+      If you want to require TSIG in access control, a system wide TSIG
+      "key ring" must be configured.
       For example, to change the previous example to allowing requests
       from 192.0.2.1 signed by a TSIG with a key name of
       "key.example", you'll need to do this:
     </para>
 
     <screen>&gt; <userinput>config set tsig_keys/keys ["key.example:&lt;base64-key&gt;"]</userinput>
-&gt; <userinput>config set Xfrout/tsig_keys/keys ["key.example:&lt;base64-key&gt;"]</userinput>
 &gt; <userinput>config set Xfrout/zone_config[0]/transfer_acl [{"action": "ACCEPT", "from": "192.0.2.1", "key": "key.example"}]</userinput>
 &gt; <userinput>config commit</userinput></screen>
 
-    <para>
-      The first line of configuration defines a system wide key ring.
-      This is necessary because the <command>b10-auth</command> server
-      also checks TSIGs and it uses the system wide configuration.
-    </para>
+    <para>Both Xfrout and Auth will use the system wide keyring to check
+    TSIGs in the incomming messages and to sign responses.</para>
 
     <note><simpara>
-        In a future version, <command>b10-xfrout</command> will also
-        use the system wide TSIG configuration.
         The way to specify zone specific configuration (ACLs, etc) is
-        likely to be changed, too.
+        likely to be changed.
     </simpara></note>
 
 <!--

+ 0 - 14
src/bin/xfrout/b10-xfrout.xml

@@ -98,13 +98,6 @@
       that can run concurrently. The default is 10.
     </para>
     <para>
-      <varname>tsig_key_ring</varname>
-      A list of TSIG keys (each of which is in the form of
-      <replaceable>name:base64-key[:algorithm]</replaceable>)
-      used for access control on transfer requests.
-      The default is an empty list.
-    </para>
-    <para>
       <varname>transfer_acl</varname>
       A list of ACL elements that apply to all transfer requests by
       default (unless overridden in <varname>zone_config</varname>).
@@ -160,13 +153,6 @@
     </simpara></note>
 
 
-<!--
-
-tsig_key_ring list of
-tsig_key string
-
--->
-
 <!-- TODO: formating -->
     <para>
       The configuration commands are:

+ 35 - 15
src/bin/xfrout/tests/xfrout_test.py.in

@@ -28,6 +28,7 @@ from xfrout import *
 import xfrout
 import isc.log
 import isc.acl.dns
+import isc.server_common.tsig_keyring
 
 TESTDATA_SRCDIR = os.getenv("TESTDATASRCDIR")
 TSIG_KEY = TSIGKey("example.com:SFuWd/q99SzF8Yzd1QbB9g==")
@@ -1155,6 +1156,39 @@ class TestUnixSockServer(unittest.TestCase):
         self.write_sock, self.read_sock = socket.socketpair()
         self.unix = MyUnixSockServer()
 
+    def test_tsig_keyring(self):
+        """
+        Check we use the global keyring when starting a request.
+        """
+        try:
+            # These are just so the keyring can be started
+            self.unix._cc.add_remote_config_by_name = \
+                lambda name, callback: None
+            self.unix._cc.get_remote_config_value = \
+                lambda module, name: ([], True)
+            self.unix._cc.remove_remote_config = lambda name: None
+            isc.server_common.tsig_keyring.init_keyring(self.unix._cc)
+            # These are not really interesting for the test. These are just
+            # handled over, so strings are OK.
+            self.unix._guess_remote = lambda sock: "Address"
+            self.unix._zone_config = "Zone config"
+            self.unix._acl = "acl"
+            # This would be the handler class, but we just check it is passed
+            # the right parametes, so function is enough for that.
+            keys = isc.server_common.tsig_keyring.get_keyring()
+            def handler(sock, data, server, keyring, address, acl, config):
+                self.assertEqual("sock", sock)
+                self.assertEqual("data", data)
+                self.assertEqual(self.unix, server)
+                self.assertEqual(keys, keyring)
+                self.assertEqual("Address", address)
+                self.assertEqual("acl", acl)
+                self.assertEqual("Zone config", config)
+            self.unix.RequestHandlerClass = handler
+            self.unix.finish_request("sock", "data")
+        finally:
+            isc.server_common.tsig_keyring.deinit_keyring()
+
     def test_guess_remote(self):
         """Test we can guess the remote endpoint when we have only the
            file descriptor. This is needed, because we get only that one
@@ -1214,25 +1248,12 @@ class TestUnixSockServer(unittest.TestCase):
 
     def test_update_config_data(self):
         self.check_default_ACL()
-        tsig_key_str = 'example.com:SFuWd/q99SzF8Yzd1QbB9g=='
-        tsig_key_list = [tsig_key_str]
-        bad_key_list = ['bad..example.com:SFuWd/q99SzF8Yzd1QbB9g==']
         self.unix.update_config_data({'transfers_out':10 })
         self.assertEqual(self.unix._max_transfers_out, 10)
-        self.assertTrue(self.unix.tsig_key_ring is not None)
         self.check_default_ACL()
 
-        self.unix.update_config_data({'transfers_out':9,
-                                      'tsig_key_ring':tsig_key_list})
+        self.unix.update_config_data({'transfers_out':9})
         self.assertEqual(self.unix._max_transfers_out, 9)
-        self.assertEqual(self.unix.tsig_key_ring.size(), 1)
-        self.unix.tsig_key_ring.remove(Name("example.com."))
-        self.assertEqual(self.unix.tsig_key_ring.size(), 0)
-
-        # bad tsig key
-        config_data = {'transfers_out':9, 'tsig_key_ring': bad_key_list}
-        self.assertRaises(None, self.unix.update_config_data(config_data))
-        self.assertEqual(self.unix.tsig_key_ring.size(), 0)
 
         # Load the ACL
         self.unix.update_config_data({'transfer_acl': [{'from': '127.0.0.1',
@@ -1449,7 +1470,6 @@ class TestXfroutServer(unittest.TestCase):
         self.assertTrue(self.xfrout_server._notifier.shutdown_called)
         self.assertTrue(self.xfrout_server._cc.stopped)
 
-
 if __name__== "__main__":
     isc.log.resetUnitTestRootLogger()
     unittest.main()

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

@@ -34,6 +34,7 @@ import select
 import errno
 from optparse import OptionParser, OptionValueError
 from isc.util import socketserver_mixin
+import isc.server_common.tsig_keyring
 
 from isc.log_messages.xfrout_messages import *
 
@@ -769,7 +770,7 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
         zone_config = self._zone_config
         self._lock.release()
         self.RequestHandlerClass(sock_fd, request_data, self,
-                                 self.tsig_key_ring,
+                                 isc.server_common.tsig_keyring.get_keyring(),
                                  self._guess_remote(sock_fd), acl, zone_config)
 
     def _remove_unused_sock_file(self, sock_file):
@@ -833,7 +834,6 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
             self._acl = new_acl
             self._zone_config = new_zone_config
             self._max_transfers_out = new_config.get('transfers_out')
-            self.set_tsig_key_ring(new_config.get('tsig_key_ring'))
         except Exception as e:
             self._lock.release()
             raise e
@@ -870,21 +870,6 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
                                             zclass_str + ': ' + str(e))
         return new_config
 
-    def set_tsig_key_ring(self, key_list):
-        """Set the tsig_key_ring , given a TSIG key string list representation. """
-
-        # XXX add values to configure zones/tsig options
-        self.tsig_key_ring = TSIGKeyRing()
-        # If key string list is empty, create a empty tsig_key_ring
-        if not key_list:
-            return
-
-        for key_item in key_list:
-            try:
-                self.tsig_key_ring.add(TSIGKey(key_item))
-            except InvalidParameter as ipe:
-                logger.error(XFROUT_BAD_TSIG_KEY_STRING, str(key_item))
-
     def get_db_file(self):
         file, is_default = self._cc.get_remote_config_value("Auth", "database_file")
         # this too should be unnecessary, but currently the
@@ -920,7 +905,8 @@ class XfroutServer:
         self._cc = isc.config.ModuleCCSession(SPECFILE_LOCATION, self.config_handler, self.command_handler)
         self._config_data = self._cc.get_full_config()
         self._cc.start()
-        self._cc.add_remote_config(AUTH_SPECFILE_LOCATION);
+        self._cc.add_remote_config(AUTH_SPECFILE_LOCATION)
+        isc.server_common.tsig_keyring.init_keyring(self._cc)
         self._start_xfr_query_listener()
         self._start_notifier()
 

+ 0 - 12
src/bin/xfrout/xfrout.spec.pre.in

@@ -39,18 +39,6 @@
          "item_default": 1048576
        },
        {
-         "item_name": "tsig_key_ring",
-         "item_type": "list",
-         "item_optional": true,
-         "item_default": [],
-         "list_item_spec" :
-         {
-             "item_name": "tsig_key",
-             "item_type": "string",
-             "item_optional": true
-         }
-       },
-       {
          "item_name": "transfer_acl",
          "item_type": "list",
          "item_optional": false,

+ 1 - 1
src/lib/python/isc/Makefile.am

@@ -1,5 +1,5 @@
 SUBDIRS = datasrc cc config dns log net notify util testutils acl bind10
-SUBDIRS += xfrin log_messages
+SUBDIRS += xfrin log_messages server_common
 
 python_PYTHON = __init__.py
 

+ 74 - 19
src/lib/python/isc/config/ccsession.py

@@ -38,6 +38,7 @@
 
 from isc.cc import Session
 from isc.config.config_data import ConfigData, MultiConfigData, BIND10_CONFIG_DATA_VERSION
+import isc.config.module_spec
 import isc
 from isc.util.file import path_search
 import bind10_config
@@ -327,43 +328,97 @@ class ModuleCCSession(ConfigData):
            and return an answer created with create_answer()"""
         self._command_handler = command_handler
 
-    def add_remote_config(self, spec_file_name, config_update_callback = None):
-        """Gives access to the configuration of a different module.
-           These remote module options can at this moment only be
-           accessed through get_remote_config_value(). This function
-           also subscribes to the channel of the remote module name
-           to receive the relevant updates. It is not possible to
-           specify your own handler for this right now.
-           start() must have been called on this CCSession
-           prior to the call to this method.
-           Returns the name of the module."""
-        module_spec = isc.config.module_spec_from_file(spec_file_name)
+    def _add_remote_config_internal(self, module_spec,
+                                    config_update_callback=None):
+        """The guts of add_remote_config and add_remote_config_by_name"""
         module_cfg = ConfigData(module_spec)
         module_name = module_spec.get_module_name()
+
         self._session.group_subscribe(module_name)
 
         # Get the current config for that module now
         seq = self._session.group_sendmsg(create_command(COMMAND_GET_CONFIG, { "module_name": module_name }), "ConfigManager")
 
         try:
-            answer, env = self._session.group_recvmsg(False, seq)
+            answer, _ = self._session.group_recvmsg(False, seq)
         except isc.cc.SessionTimeout:
             raise ModuleCCSessionError("No answer from ConfigManager when "
                                        "asking about Remote module " +
                                        module_name)
+        call_callback = False
         if answer:
             rcode, value = parse_answer(answer)
             if rcode == 0:
-                if value != None and module_spec.validate_config(False, value):
-                    module_cfg.set_local_config(value)
-                    if config_update_callback is not None:
-                        config_update_callback(value, module_cfg)
+                if value != None:
+                    if module_spec.validate_config(False, value):
+                        module_cfg.set_local_config(value)
+                        call_callback = True
+                    else:
+                        raise ModuleCCSessionError("Bad config data for " +
+                                                   module_name + ": " +
+                                                   str(value))
+            else:
+                raise ModuleCCSessionError("Failure requesting remote " +
+                                           "configuration data for " +
+                                           module_name)
 
         # all done, add it
         self._remote_module_configs[module_name] = module_cfg
         self._remote_module_callbacks[module_name] = config_update_callback
+        if call_callback and config_update_callback is not None:
+            config_update_callback(value, module_cfg)
+
+    def add_remote_config_by_name(self, module_name,
+                                  config_update_callback=None):
+        """
+        This does the same as add_remote_config, but you provide the module name
+        instead of the name of the spec file.
+        """
+        seq = self._session.group_sendmsg(create_command(COMMAND_GET_MODULE_SPEC,
+                                                         { "module_name":
+                                                         module_name }),
+                                          "ConfigManager")
+        try:
+            answer, env = self._session.group_recvmsg(False, seq)
+        except isc.cc.SessionTimeout:
+            raise ModuleCCSessionError("No answer from ConfigManager when " +
+                                       "asking about for spec of Remote " +
+                                       "module " + module_name)
+        if answer:
+            rcode, value = parse_answer(answer)
+            if rcode == 0:
+                module_spec = isc.config.module_spec.ModuleSpec(value)
+                if module_spec.get_module_name() != module_name:
+                    raise ModuleCCSessionError("Module name mismatch: " +
+                                               module_name + " and " +
+                                               module_spec.get_module_name())
+                self._add_remote_config_internal(module_spec,
+                                                 config_update_callback)
+            else:
+                raise ModuleCCSessionError("Error code " + str(rcode) +
+                                           "when asking for module spec of " +
+                                           module_name)
+        else:
+            raise ModuleCCSessionError("No answer when asking for module " +
+                                       "spec of " + module_name)
+        # Just to be consistent with the add_remote_config
         return module_name
-        
+
+    def add_remote_config(self, spec_file_name, config_update_callback=None):
+        """Gives access to the configuration of a different module.
+           These remote module options can at this moment only be
+           accessed through get_remote_config_value(). This function
+           also subscribes to the channel of the remote module name
+           to receive the relevant updates. It is not possible to
+           specify your own handler for this right now, but you can
+           specify a callback that is called after the change happened.
+           start() must have been called on this CCSession
+           prior to the call to this method.
+           Returns the name of the module."""
+        module_spec = isc.config.module_spec_from_file(spec_file_name)
+        self._add_remote_config_internal(module_spec, config_update_callback)
+        return module_spec.get_module_name()
+
     def remove_remote_config(self, module_name):
         """Removes the remote configuration access for this module"""
         if module_name in self._remote_module_configs:
@@ -501,8 +556,8 @@ class UIModuleCCSession(MultiConfigData):
                 self.set_value(identifier, cur_map)
             else:
                 raise isc.cc.data.DataAlreadyPresentError(value +
-                                                          " already in "
-                                                          + identifier)
+                                                          " already in " +
+                                                          identifier)
 
     def add_value(self, identifier, value_str = None, set_value_str = None):
         """Add a value to a configuration list. Raises a DataTypeError

+ 271 - 52
src/lib/python/isc/config/tests/ccsession_test.py

@@ -488,45 +488,6 @@ class TestModuleCCSession(unittest.TestCase):
         self.assertEqual({'result': [0]},
                          fake_session.get_message('Spec2', None))
  
-    def test_check_command_without_recvmsg_remote_module(self):
-        "copied from test_check_command3"
-        fake_session = FakeModuleCCSession()
-        mccs = self.create_session("spec1.spec", None, None, fake_session)
-        mccs.set_config_handler(self.my_config_handler_ok)
-        self.assertEqual(len(fake_session.message_queue), 0)
-
-        fake_session.group_sendmsg(None, 'Spec2')
-        rmodname = mccs.add_remote_config(self.spec_file("spec2.spec"))
-        print(fake_session.message_queue)
-        self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
-                         fake_session.get_message('ConfigManager', None))
-        self.assertEqual(len(fake_session.message_queue), 0)
-
-        cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, { 'Spec2': { 'item1': 2 }})
-        env = { 'group':'Spec2', 'from':None }
-        self.assertEqual(len(fake_session.message_queue), 0)
-        mccs.check_command_without_recvmsg(cmd, env)
-        self.assertEqual(len(fake_session.message_queue), 0)
- 
-    def test_check_command_without_recvmsg_remote_module2(self):
-        "copied from test_check_command3"
-        fake_session = FakeModuleCCSession()
-        mccs = self.create_session("spec1.spec", None, None, fake_session)
-        mccs.set_config_handler(self.my_config_handler_ok)
-        self.assertEqual(len(fake_session.message_queue), 0)
-
-        fake_session.group_sendmsg(None, 'Spec2')
-        rmodname = mccs.add_remote_config(self.spec_file("spec2.spec"))
-        self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
-                         fake_session.get_message('ConfigManager', None))
-        self.assertEqual(len(fake_session.message_queue), 0)
-
-        cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, { 'Spec3': { 'item1': 2 }})
-        env = { 'group':'Spec3', 'from':None }
-        self.assertEqual(len(fake_session.message_queue), 0)
-        mccs.check_command_without_recvmsg(cmd, env)
-        self.assertEqual(len(fake_session.message_queue), 0)
- 
     def test_check_command_block_timeout(self):
         """Check it works if session has timeout and it sets it back."""
         def cmd_check(mccs, session):
@@ -554,16 +515,65 @@ class TestModuleCCSession(unittest.TestCase):
         mccs.set_command_handler(self.my_command_handler_ok)
         self.assertRaises(WouldBlockForever, lambda: mccs.check_command(False))
 
-    def test_remote_module(self):
+    # Now there's a group of tests testing both add_remote_config and
+    # add_remote_config_by_name. Since they are almost the same (they differ
+    # just in the parameter and that the second one asks one more question over
+    # the bus), the actual test code is shared.
+    #
+    # These three functions are helper functions to easy up the writing of them.
+    # To write a test, there need to be 3 functions. First, the function that
+    # does the actual test. It looks like:
+    # def _internal_test(self, function_lambda, param, fill_other_messages):
+    #
+    # The function_lambda provides the tested function if called on the
+    # ccsession. The param is the parameter to pass to the function (either
+    # the module name or the spec file name. The fill_other_messages fills
+    # needed messages (the answer containing the module spec in case of add by
+    # name, no messages in the case of adding by spec file) into the fake bus.
+    # So, the code would look like:
+    #
+    # * Create the fake session and tested ccsession object
+    # * function = function_lambda(ccsession object)
+    # * fill_other_messages(fake session)
+    # * Fill in answer to the get_module_config command
+    # * Test by calling function(param)
+    #
+    # Then you need two wrappers that do launch the tests. There are helpers
+    # for that, so you can just call:
+    # def test_by_spec(self)
+    #     self._common_remote_module_test(self._internal_test)
+    # def test_by_name(self)
+    #     self._common_remote_module_by_name_test(self._internal_test)
+    def _common_remote_module_test(self, internal_test):
+        internal_test(lambda ccs: ccs.add_remote_config,
+                      self.spec_file("spec2.spec"),
+                      lambda session: None)
+
+    def _prepare_spec_message(self, session, spec_name):
+        # It could have been one command, but the line would be way too long
+        # to even split it
+        spec_file = self.spec_file(spec_name)
+        spec = isc.config.module_spec_from_file(spec_file)
+        session.group_sendmsg({'result': [0, spec.get_full_spec()]}, "Spec1")
+
+    def _common_remote_module_by_name_test(self, internal_test):
+        internal_test(lambda ccs: ccs.add_remote_config_by_name, "Spec2",
+                      lambda session: self._prepare_spec_message(session,
+                                                                 "spec2.spec"))
+
+    def _internal_remote_module(self, function_lambda, parameter,
+                                fill_other_messages):
         fake_session = FakeModuleCCSession()
         mccs = self.create_session("spec1.spec", None, None, fake_session)
         mccs.remove_remote_config("Spec2")
+        function = function_lambda(mccs)
 
         self.assertRaises(ModuleCCSessionError, mccs.get_remote_config_value, "Spec2", "item1")
 
         self.assertFalse("Spec2" in fake_session.subscriptions)
+        fill_other_messages(fake_session)
         fake_session.group_sendmsg(None, 'Spec2')
-        rmodname = mccs.add_remote_config(self.spec_file("spec2.spec"))
+        rmodname = function(parameter)
         self.assertTrue("Spec2" in fake_session.subscriptions)
         self.assertEqual("Spec2", rmodname)
         self.assertRaises(isc.cc.data.DataNotFoundError, mccs.get_remote_config_value, rmodname, "asdf")
@@ -575,36 +585,77 @@ class TestModuleCCSession(unittest.TestCase):
         self.assertFalse("Spec2" in fake_session.subscriptions)
         self.assertRaises(ModuleCCSessionError, mccs.get_remote_config_value, "Spec2", "item1")
 
-        # test if unsubscription is alse sent when object is deleted
+        # test if unsubscription is also sent when object is deleted
+        fill_other_messages(fake_session)
         fake_session.group_sendmsg({'result' : [0]}, 'Spec2')
-        rmodname = mccs.add_remote_config(self.spec_file("spec2.spec"))
+        rmodname = function(parameter)
         self.assertTrue("Spec2" in fake_session.subscriptions)
         mccs = None
+        function = None
         self.assertFalse("Spec2" in fake_session.subscriptions)
 
-    def test_remote_module_with_custom_config(self):
+    def test_remote_module(self):
+        """
+        Test we can add a remote config and get the configuration.
+        Remote module specified by the spec file name.
+        """
+        self._common_remote_module_test(self._internal_remote_module)
+
+    def test_remote_module_by_name(self):
+        """
+        Test we can add a remote config and get the configuration.
+        Remote module specified its name.
+        """
+        self._common_remote_module_by_name_test(self._internal_remote_module)
+
+    def _internal_remote_module_with_custom_config(self, function_lambda,
+                                                   parameter,
+                                                   fill_other_messages):
         fake_session = FakeModuleCCSession()
         mccs = self.create_session("spec1.spec", None, None, fake_session)
-        # override the default config value for "item1".  add_remote_config()
-        # should incorporate the overridden value, and we should be abel to
+        function = function_lambda(mccs)
+        # override the default config value for "item1". add_remote_config[_by_name]()
+        # should incorporate the overridden value, and we should be able to
         # get it via get_remote_config_value().
+        fill_other_messages(fake_session)
         fake_session.group_sendmsg({'result': [0, {"item1": 10}]}, 'Spec2')
-        rmodname = mccs.add_remote_config(self.spec_file("spec2.spec"))
+        rmodname = function(parameter)
         value, default = mccs.get_remote_config_value(rmodname, "item1")
         self.assertEqual(10, value)
         self.assertEqual(False, default)
 
-    def test_ignore_command_remote_module(self):
+    def test_remote_module_with_custom_config(self):
+        """
+        Test the config of module will load non-default values on
+        initialization.
+        Remote module specified by the spec file name.
+        """
+        self._common_remote_module_test(
+            self._internal_remote_module_with_custom_config)
+
+    def test_remote_module_by_name_with_custom_config(self):
+        """
+        Test the config of module will load non-default values on
+        initialization.
+        Remote module its name.
+        """
+        self._common_remote_module_by_name_test(
+            self._internal_remote_module_with_custom_config)
+
+    def _internal_ignore_command_remote_module(self, function_lambda, param,
+                                               fill_other_messages):
         # Create a Spec1 module and subscribe to remote config for Spec2
         fake_session = FakeModuleCCSession()
         mccs = self.create_session("spec1.spec", None, None, fake_session)
         mccs.set_command_handler(self.my_command_handler_ok)
+        function = function_lambda(mccs)
+        fill_other_messages(fake_session)
         fake_session.group_sendmsg(None, 'Spec2')
-        rmodname = mccs.add_remote_config(self.spec_file("spec2.spec"))
+        rmodname = function(param)
 
-        # remove the 'get config' from the queue
-        self.assertEqual(len(fake_session.message_queue), 1)
-        fake_session.get_message("ConfigManager")
+        # remove the commands from queue
+        while len(fake_session.message_queue) > 0:
+            fake_session.get_message("ConfigManager")
 
         # check if the command for the module itself is received
         cmd = isc.config.ccsession.create_command("just_some_command", { 'foo': 'a' })
@@ -622,6 +673,174 @@ class TestModuleCCSession(unittest.TestCase):
         mccs.check_command()
         self.assertEqual(len(fake_session.message_queue), 0)
 
+    def test_ignore_commant_remote_module(self):
+        """
+        Test that commands for remote modules aren't handled.
+        Remote module specified by the spec file name.
+        """
+        self._common_remote_module_test(
+            self._internal_ignore_command_remote_module)
+
+    def test_ignore_commant_remote_module_by_name(self):
+        """
+        Test that commands for remote modules aren't handled.
+        Remote module specified by its name.
+        """
+        self._common_remote_module_by_name_test(
+            self._internal_ignore_command_remote_module)
+
+    def _internal_check_command_without_recvmsg_remote_module(self,
+                                                              function_lambda,
+                                                              param,
+                                                              fill_other_messages):
+        fake_session = FakeModuleCCSession()
+        mccs = self.create_session("spec1.spec", None, None, fake_session)
+        mccs.set_config_handler(self.my_config_handler_ok)
+        function = function_lambda(mccs)
+        self.assertEqual(len(fake_session.message_queue), 0)
+
+        fill_other_messages(fake_session)
+        fake_session.group_sendmsg(None, 'Spec2')
+        rmodname = function(param)
+        if (len(fake_session.message_queue) == 2):
+            self.assertEqual({'command': ['get_module_spec',
+                                          {'module_name': 'Spec2'}]},
+                             fake_session.get_message('ConfigManager', None))
+        self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
+                         fake_session.get_message('ConfigManager', None))
+        self.assertEqual(len(fake_session.message_queue), 0)
+
+        cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, { 'Spec2': { 'item1': 2 }})
+        env = { 'group':'Spec2', 'from':None }
+        self.assertEqual(len(fake_session.message_queue), 0)
+        mccs.check_command_without_recvmsg(cmd, env)
+        self.assertEqual(len(fake_session.message_queue), 0)
+
+    def test_check_command_without_recvmsg_remote_module(self):
+        """
+        Test updates on remote module.
+        The remote module is specified by the spec file name.
+        """
+        self._common_remote_module_test(
+            self._internal_check_command_without_recvmsg_remote_module)
+
+    def test_check_command_without_recvmsg_remote_module_by_name(self):
+        """
+        Test updates on remote module.
+        The remote module is specified by its name.
+        """
+        self._common_remote_module_by_name_test(
+            self._internal_check_command_without_recvmsg_remote_module)
+
+    def _internal_check_command_without_recvmsg_remote_module2(self,
+                                                               function_lambda,
+                                                               param,
+                                                               fill_other_messages):
+        fake_session = FakeModuleCCSession()
+        mccs = self.create_session("spec1.spec", None, None, fake_session)
+        mccs.set_config_handler(self.my_config_handler_ok)
+        function = function_lambda(mccs)
+        self.assertEqual(len(fake_session.message_queue), 0)
+
+        fill_other_messages(fake_session)
+        fake_session.group_sendmsg(None, 'Spec2')
+        rmodname = function(param)
+        if (len(fake_session.message_queue) == 2):
+            self.assertEqual({'command': ['get_module_spec',
+                                          {'module_name': 'Spec2'}]},
+                             fake_session.get_message('ConfigManager', None))
+        self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
+                         fake_session.get_message('ConfigManager', None))
+        self.assertEqual(len(fake_session.message_queue), 0)
+
+        cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, { 'Spec3': { 'item1': 2 }})
+        env = { 'group':'Spec3', 'from':None }
+        self.assertEqual(len(fake_session.message_queue), 0)
+        mccs.check_command_without_recvmsg(cmd, env)
+        self.assertEqual(len(fake_session.message_queue), 0)
+
+    def test_check_command_without_recvmsg_remote_module2(self):
+        """
+        Test updates on remote module.
+        The remote module is specified by the spec file name.
+        """
+        self._common_remote_module_test(
+            self._internal_check_command_without_recvmsg_remote_module2)
+
+    def test_check_command_without_recvmsg_remote_module_by_name2(self):
+        """
+        Test updates on remote module.
+        The remote module is specified by its name.
+        """
+        self._common_remote_module_by_name_test(
+            self._internal_check_command_without_recvmsg_remote_module2)
+
+    def _internal_remote_module_bad_config(self, function_lambda, parameter,
+                                           fill_other_messages):
+        fake_session = FakeModuleCCSession()
+        mccs = self.create_session("spec1.spec", None, None, fake_session)
+        function = function_lambda(mccs)
+        # Provide wrong config data. It should be rejected.
+        fill_other_messages(fake_session)
+        fake_session.group_sendmsg({'result': [0, {"bad_item": -1}]}, 'Spec2')
+        self.assertRaises(isc.config.ModuleCCSessionError,
+                          function, parameter)
+
+    def test_remote_module_bad_config(self):
+        """
+        Test the remote module rejects bad config data.
+        """
+        self._common_remote_module_test(
+            self._internal_remote_module_bad_config)
+
+    def test_remote_module_by_name_bad_config(self):
+        """
+        Test the remote module rejects bad config data.
+        """
+        self._common_remote_module_by_name_test(
+            self._internal_remote_module_bad_config)
+
+    def _internal_remote_module_error_response(self, function_lambda,
+                                               parameter, fill_other_messages):
+        fake_session = FakeModuleCCSession()
+        mccs = self.create_session("spec1.spec", None, None, fake_session)
+        function = function_lambda(mccs)
+        # Provide wrong config data. It should be rejected.
+        fill_other_messages(fake_session)
+        fake_session.group_sendmsg({'result': [1, "An error, and I mean it!"]},
+                                   'Spec2')
+        self.assertRaises(isc.config.ModuleCCSessionError,
+                          function, parameter)
+
+    def test_remote_module_bad_config(self):
+        """
+        Test the remote module complains if there's an error response."
+        """
+        self._common_remote_module_test(
+            self._internal_remote_module_error_response)
+
+    def test_remote_module_by_name_bad_config(self):
+        """
+        Test the remote module complains if there's an error response."
+        """
+        self._common_remote_module_by_name_test(
+            self._internal_remote_module_error_response)
+
+    def test_remote_module_bad_config(self):
+        """
+        Test the remote module rejects bad config data.
+        """
+        self._common_remote_module_by_name_test(
+            self._internal_remote_module_bad_config)
+
+    def test_module_name_mismatch(self):
+        fake_session = FakeModuleCCSession()
+        mccs = self.create_session("spec1.spec", None, None, fake_session)
+        mccs.set_config_handler(self.my_config_handler_ok)
+        self._prepare_spec_message(fake_session, 'spec1.spec')
+        self.assertRaises(isc.config.ModuleCCSessionError,
+                          mccs.add_remote_config_by_name, "Spec2")
+
     def test_logconfig_handler(self):
         # test whether default_logconfig_handler reacts nicely to
         # bad data. We assume the actual logger output is tested

+ 2 - 0
src/lib/python/isc/log_messages/Makefile.am

@@ -13,6 +13,7 @@ EXTRA_DIST += cfgmgr_messages.py
 EXTRA_DIST += config_messages.py
 EXTRA_DIST += notify_out_messages.py
 EXTRA_DIST += libxfrin_messages.py
+EXTRA_DIST += server_common_messages.py
 
 CLEANFILES = __init__.pyc
 CLEANFILES += bind10_messages.pyc
@@ -27,6 +28,7 @@ CLEANFILES += cfgmgr_messages.pyc
 CLEANFILES += config_messages.pyc
 CLEANFILES += notify_out_messages.pyc
 CLEANFILES += libxfrin_messages.pyc
+CLEANFILES += server_common_messages.pyc
 
 CLEANDIRS = __pycache__
 

+ 1 - 0
src/lib/python/isc/log_messages/server_common_messages.py

@@ -0,0 +1 @@
+from work.server_common_messages import *

+ 24 - 0
src/lib/python/isc/server_common/Makefile.am

@@ -0,0 +1,24 @@
+SUBDIRS = tests
+
+python_PYTHON = __init__.py tsig_keyring.py
+
+pythondir = $(pyexecdir)/isc/server_common
+
+BUILT_SOURCES = $(PYTHON_LOGMSGPKG_DIR)/work/server_common_messages.py
+nodist_pylogmessage_PYTHON = $(PYTHON_LOGMSGPKG_DIR)/work/server_common_messages.py
+
+pylogmessagedir = $(pyexecdir)/isc/logmessages/
+
+CLEANFILES = $(PYTHON_LOGMSGPKG_DIR)/work/server_common_messages.py
+CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/server_common_messages.pyc
+
+CLEANDIRS = __pycache__
+
+EXTRA_DIST = server_common_messages.mes
+
+$(PYTHON_LOGMSGPKG_DIR)/work/server_common_messages.py : server_common_messages.mes
+	$(top_builddir)/src/lib/log/compiler/message \
+	-d $(PYTHON_LOGMSGPKG_DIR)/work -p $(srcdir)/server_common_messages.mes
+
+clean-local:
+	rm -rf $(CLEANDIRS)

+ 0 - 0
src/lib/python/isc/server_common/__init__.py


+ 36 - 0
src/lib/python/isc/server_common/server_common_messages.mes

@@ -0,0 +1,36 @@
+# Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+# No namespace declaration - these constants go in the global namespace
+# of the config_messages python module.
+
+# since these messages are for the python server_common library, care must
+# be taken that names do not conflict with the messages from the c++
+# server_common library. A checker script should verify that, but we do not
+# have that at this moment. So when adding a message, make sure that
+# the name is not already used in src/lib/config/config_messages.mes
+
+% PYSERVER_COMMON_TSIG_KEYRING_DEINIT Deinitializing global TSIG keyring
+A debug message noting that the global TSIG keyring is being removed from
+memory. Most programs don't do that, they just exit, which is OK.
+
+% PYSERVER_COMMON_TSIG_KEYRING_INIT Initializing global TSIG keyring
+A debug message noting the TSIG keyring storage is being prepared. It should
+appear at most once in the lifetime of a program. The keyring still needs
+to be loaded from configuration.
+
+% PYSERVER_COMMON_TSIG_KEYRING_UPDATE Updating global TSIG keyring
+A debug message. The TSIG keyring is being (re)loaded from configuration.
+This happens at startup or when the configuration changes. The old keyring
+is removed and new one created with all the keys.

+ 24 - 0
src/lib/python/isc/server_common/tests/Makefile.am

@@ -0,0 +1,24 @@
+PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
+PYTESTS = tsig_keyring_test.py
+EXTRA_DIST = $(PYTESTS)
+
+# If necessary (rare cases), explicitly specify paths to dynamic libraries
+# required by loadable python modules.
+LIBRARY_PATH_PLACEHOLDER =
+if SET_ENV_LIBRARY_PATH
+LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
+endif
+
+# test using command-line arguments, so use check-local target instead of TESTS
+check-local:
+if ENABLE_PYTHON_COVERAGE
+	touch $(abs_top_srcdir)/.coverage
+	rm -f .coverage
+	${LN_S} $(abs_top_srcdir)/.coverage .coverage
+endif
+	for pytest in $(PYTESTS) ; do \
+	echo Running test: $$pytest ; \
+	$(LIBRARY_PATH_PLACEHOLDER) \
+	PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/lib/dns/python/.libs \
+	$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
+	done

+ 193 - 0
src/lib/python/isc/server_common/tests/tsig_keyring_test.py

@@ -0,0 +1,193 @@
+# Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""
+Tests for isc.server_common.tsig_keyring.
+"""
+
+import unittest
+import isc.log
+from isc.server_common.tsig_keyring import *
+import isc.dns
+from isc.testutils.ccsession_mock import MockModuleCCSession
+
+class Session(MockModuleCCSession):
+    """
+    A class pretending to be the config session.
+    """
+    def __init__(self):
+        MockModuleCCSession.__init__(self)
+        self._name = None
+        self._callback = None
+        self._remove_name = None
+        self._data = None
+
+    def add_remote_config_by_name(self, name, callback):
+        self._name = name
+        self._callback = callback
+
+    def remove_remote_config(self, name):
+        self._remove_name = name
+
+    def get_remote_config_value(self, module, name):
+        if module != 'tsig_keys' or name != 'keys':
+            raise Exception("Asked for bad data element")
+        return (self._data, False)
+
+class TSIGKeyRingTest(unittest.TestCase):
+    """
+    Tests for the isc.server_common.tsig_keyring module.
+    """
+    def setUp(self):
+        self.__session = Session()
+        self.__sha1name = isc.dns.Name('hmac-sha1')
+        self.__md5name = isc.dns.Name('hmac-md5.sig-alg.reg.int')
+
+    def tearDown(self):
+        deinit_keyring()
+
+    def __do_init(self):
+        init_keyring(self.__session)
+        # Some initialization happened
+        self.assertEqual('tsig_keys', self.__session._name)
+
+    def test_initialization(self):
+        """
+        Test we can initialize and deintialize the keyring. It also
+        tests the interaction with the keyring() function.
+        """
+        # The keyring function raises until initialized
+        self.assertRaises(Unexpected, get_keyring)
+        self.__do_init()
+        current_keyring = get_keyring()
+        self.assertTrue(isinstance(current_keyring, isc.dns.TSIGKeyRing))
+        # Another initialization does nothing
+        self.__do_init()
+        self.assertEqual(current_keyring, get_keyring())
+        # When we deinitialize it, it no longer provides the keyring
+        deinit_keyring()
+        self.assertEqual('tsig_keys', self.__session._remove_name)
+        self.__session._remove_name = None
+        self.assertRaises(Unexpected, get_keyring)
+        # Another deinitialization doesn't change anything
+        deinit_keyring()
+        self.assertRaises(Unexpected, get_keyring)
+        self.assertIsNone(self.__session._remove_name)
+        # Test we can init it again (not expected, but not forbidden)
+        self.__do_init()
+        self.assertTrue(isinstance(get_keyring(), isc.dns.TSIGKeyRing))
+
+    def test_load(self):
+        """
+        Test it can load the keys from the configuration and reload them
+        when the data change.
+        """
+        # Initial load
+        self.__session._data = ['key:MTIzNAo=:hmac-sha1']
+        self.__do_init()
+        keys = get_keyring()
+        self.assertEqual(1, keys.size())
+        (rcode, key) = keys.find(isc.dns.Name('key'), self.__sha1name)
+        self.assertEqual(isc.dns.TSIGKeyRing.SUCCESS, rcode)
+        self.assertEqual(isc.dns.Name('key'), key.get_key_name())
+        # There's a change in the configuration
+        # (The key has a different name)
+        self.__session._data = ['key.example:MTIzNAo=:hmac-sha1']
+        self.__session._callback()
+        orig_keys = keys
+        keys = get_keyring()
+        self.assertNotEqual(keys, orig_keys)
+        self.assertEqual(1, keys.size())
+        # The old key is not here
+        (rcode, key) = keys.find(isc.dns.Name('key'), self.__sha1name)
+        self.assertEqual(isc.dns.TSIGKeyRing.NOTFOUND, rcode)
+        self.assertIsNone(key)
+        # But the new one is
+        (rcode, key) = keys.find(isc.dns.Name('key.example'), self.__sha1name)
+        self.assertEqual(isc.dns.TSIGKeyRing.SUCCESS, rcode)
+        self.assertEqual(isc.dns.Name('key.example'), key.get_key_name())
+
+    def test_empty_update(self):
+        """
+        Test an update that doesn't carry the correct element doesn't change
+        anything.
+        """
+        self.__session._data = ['key:MTIzNAo=:hmac-sha1']
+        self.__do_init()
+        keys = get_keyring()
+        self.__session._data = None
+        self.__session._callback()
+        self.assertEqual(keys, get_keyring())
+
+    def test_no_keys_update(self):
+        """
+        Test we can update the keyring to be empty.
+        """
+        self.__session._data = ['key:MTIzNAo=:hmac-sha1']
+        self.__do_init()
+        keys = get_keyring()
+        self.assertEqual(1, keys.size())
+        self.__session._data = []
+        self.__session._callback()
+        keys = get_keyring()
+        self.assertEqual(0, keys.size())
+
+    def test_update_multi(self):
+        """
+        Test we can handle multiple keys in startup/update.
+        """
+        # Init
+        self.__session._data = ['key:MTIzNAo=:hmac-sha1', 'key2:MTIzNAo=']
+        self.__do_init()
+        keys = get_keyring()
+        self.assertEqual(2, keys.size())
+        (rcode, key) = keys.find(isc.dns.Name('key'), self.__sha1name)
+        self.assertEqual(isc.dns.TSIGKeyRing.SUCCESS, rcode)
+        self.assertEqual(isc.dns.Name('key'), key.get_key_name())
+        (rcode, key) = keys.find(isc.dns.Name('key2'), self.__md5name)
+        self.assertEqual(isc.dns.TSIGKeyRing.SUCCESS, rcode)
+        self.assertEqual(isc.dns.Name('key2'), key.get_key_name())
+        # Update
+        self.__session._data = ['key1:MTIzNAo=:hmac-sha1', 'key3:MTIzNAo=']
+        self.__session._callback()
+        keys = get_keyring()
+        self.assertEqual(2, keys.size())
+        (rcode, key) = keys.find(isc.dns.Name('key1'), self.__sha1name)
+        self.assertEqual(isc.dns.TSIGKeyRing.SUCCESS, rcode)
+        self.assertEqual(isc.dns.Name('key1'), key.get_key_name())
+        (rcode, key) = keys.find(isc.dns.Name('key3'), self.__md5name)
+        self.assertEqual(isc.dns.TSIGKeyRing.SUCCESS, rcode)
+        self.assertEqual(isc.dns.Name('key3'), key.get_key_name())
+
+    def test_update_bad(self):
+        """
+        Test it raises on bad updates and doesn't change anything.
+        """
+        self.__session._data = ['key:MTIzNAo=:hmac-sha1']
+        self.__do_init()
+        keys = get_keyring()
+        # Bad TSIG string
+        self.__session._data = ['key:this makes no sense:really']
+        self.assertRaises(isc.dns.InvalidParameter, self.__session._callback)
+        self.assertEqual(keys, get_keyring())
+        # A duplicity
+        self.__session._data = ['key:MTIzNAo=:hmac-sha1', 'key:MTIzNAo=:hmac-sha1']
+        self.assertRaises(AddError, self.__session._callback)
+        self.assertEqual(keys, get_keyring())
+
+if __name__ == "__main__":
+    isc.log.init("bind10") # FIXME Should this be needed?
+    isc.log.resetUnitTestRootLogger()
+    unittest.main()

+ 121 - 0
src/lib/python/isc/server_common/tsig_keyring.py

@@ -0,0 +1,121 @@
+# Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""
+This module conveniently keeps a copy of TSIG keyring loaded from the
+tsig_keys module.
+"""
+
+import isc.dns
+import isc.log
+from isc.log_messages.server_common_messages import *
+
+updater = None
+logger = isc.log.Logger("server_common")
+
+class Unexpected(Exception):
+    """
+    Raised when an unexpected operation is requested by the user of this
+    module. For example if calling keyring() before init_keyring().
+    """
+    pass
+
+class AddError(Exception):
+    """
+    Raised when a key can not be added. This usually means there's a
+    duplicate.
+    """
+    pass
+
+class Updater:
+    """
+    The updater of tsig key ring. Not to be used directly.
+    """
+    def __init__(self, session):
+        """
+        Constructor. Pass the ccsession object so the key ring can be
+        downloaded.
+        """
+        logger.debug(logger.DBGLVL_TRACE_BASIC,
+                     PYSERVER_COMMON_TSIG_KEYRING_INIT)
+        self.__session = session
+        self.__keyring = isc.dns.TSIGKeyRing()
+        session.add_remote_config_by_name('tsig_keys', self.__update)
+        self.__update()
+
+    def __update(self, value=None, module_cfg=None):
+        """
+        Update the key ring by the configuration.
+
+        Note that this function is used as a callback, but can raise
+        on bad data. The bad data is expected to be handled by the
+        configuration plugin and not be allowed as far as here.
+
+        The parameters are there just to match the signature which
+        the callback should have (i.e. they are ignored).
+        """
+        logger.debug(logger.DBGLVL_TRACE_BASIC,
+                     PYSERVER_COMMON_TSIG_KEYRING_UPDATE)
+        (data, _) = self.__session.get_remote_config_value('tsig_keys', 'keys')
+        if data is not None: # There's an update
+            keyring = isc.dns.TSIGKeyRing()
+            for key_data in data:
+                key = isc.dns.TSIGKey(key_data)
+                if keyring.add(key) != isc.dns.TSIGKeyRing.SUCCESS:
+                    raise AddError("Can't add key " + str(key))
+            self.__keyring = keyring
+
+    def get_keyring(self):
+        """
+        Return the current key ring.
+        """
+        return self.__keyring
+
+    def deinit(self):
+        """
+        Unregister from getting updates. The object will not be
+        usable any more after this.
+        """
+        logger.debug(logger.DBGLVL_TRACE_BASIC,
+                     PYSERVER_COMMON_TSIG_KEYRING_DEINIT)
+        self.__session.remove_remote_config('tsig_keys')
+
+def get_keyring():
+    """
+    Get the current key ring. You need to call init_keyring first.
+    """
+    if updater is None:
+        raise Unexpected("You need to initialize the keyring first by " +
+                         "init_keyring()")
+    return updater.get_keyring()
+
+def init_keyring(session):
+    """
+    Initialize the key ring for future use. It does nothing if already
+    initialized.
+    """
+    global updater
+    if updater is None:
+        updater = Updater(session)
+
+def deinit_keyring():
+    """
+    Deinit key ring. Yoeu can no longer access keyring() after this.
+    Does nothing if not initialized.
+    """
+    global updater
+    if updater is not None:
+        updater.deinit()
+        updater = None