Browse Source

[1513] added notification from ddns to xfrout about zone update.

there's not directly related change: rename the internal SessionError
to InternalError, because the former could be confused with the same
name of exception defined in isc.cc.
JINMEI Tatuya 13 years ago
parent
commit
31d7b2369b
3 changed files with 189 additions and 13 deletions
  1. 53 10
      src/bin/ddns/ddns.py.in
  2. 23 0
      src/bin/ddns/ddns_messages.mes
  3. 113 3
      src/bin/ddns/tests/ddns_test.py

+ 53 - 10
src/bin/ddns/ddns.py.in

@@ -23,11 +23,12 @@ import bind10_config
 from isc.dns import *
 from isc.dns import *
 import isc.ddns.session
 import isc.ddns.session
 from isc.ddns.zone_config import ZoneConfig
 from isc.ddns.zone_config import ZoneConfig
-from isc.ddns.logger import ClientFormatter
+from isc.ddns.logger import ClientFormatter, ZoneFormatter
 from isc.config.ccsession import *
 from isc.config.ccsession import *
-from isc.cc import SessionError, SessionTimeout
+from isc.cc import SessionError, SessionTimeout, ProtocolError
 import isc.util.process
 import isc.util.process
 import isc.util.cio.socketsession
 import isc.util.cio.socketsession
+from isc.notify.notify_out import ZONE_NEW_DATA_READY_CMD
 import isc.server_common.tsig_keyring
 import isc.server_common.tsig_keyring
 from isc.datasrc import DataSourceClient
 from isc.datasrc import DataSourceClient
 import select
 import select
@@ -79,6 +80,9 @@ AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + '/auth.spec'
 
 
 isc.util.process.rename()
 isc.util.process.rename()
 
 
+# Cooperating modules
+XFROUT_MODULE_NAME = 'Xfrout'
+
 class DDNSConfigError(Exception):
 class DDNSConfigError(Exception):
     '''An exception indicating an error in updating ddns configuration.
     '''An exception indicating an error in updating ddns configuration.
 
 
@@ -193,7 +197,7 @@ class DDNSServer:
         # DDNS Protocol handling class.
         # DDNS Protocol handling class.
         self._UpdateSessionClass = isc.ddns.session.UpdateSession
         self._UpdateSessionClass = isc.ddns.session.UpdateSession
 
 
-    class SessionError(Exception):
+    class InternalError(Exception):
         '''Exception for internal errors in an update session.
         '''Exception for internal errors in an update session.
 
 
         This exception is expected to be caught within the server class,
         This exception is expected to be caught within the server class,
@@ -301,8 +305,8 @@ class DDNSServer:
                                isc.server_common.tsig_keyring.get_keyring())
                                isc.server_common.tsig_keyring.get_keyring())
         tsig_error = tsig_ctx.verify(tsig_record, req_data)
         tsig_error = tsig_ctx.verify(tsig_record, req_data)
         if tsig_error != TSIGError.NOERROR:
         if tsig_error != TSIGError.NOERROR:
-            raise SessionError("Failed to verify request's TSIG: " +
-                               str(tsig_error))
+            raise InternalError("Failed to verify request's TSIG: " +
+                                str(tsig_error))
         return tsig_ctx
         return tsig_ctx
 
 
     def handle_request(self, req_session):
     def handle_request(self, req_session):
@@ -339,13 +343,13 @@ class DDNSServer:
         # as an internal error and don't bother to respond.
         # as an internal error and don't bother to respond.
         try:
         try:
             if sock.proto == socket.IPPROTO_TCP:
             if sock.proto == socket.IPPROTO_TCP:
-                raise SessionError('TCP requests are not yet supported')
+                raise InternalError('TCP requests are not yet supported')
             self.__request_msg.clear(Message.PARSE)
             self.__request_msg.clear(Message.PARSE)
             # specify PRESERVE_ORDER as we need to handle each RR separately.
             # specify PRESERVE_ORDER as we need to handle each RR separately.
             self.__request_msg.from_wire(req_data, Message.PRESERVE_ORDER)
             self.__request_msg.from_wire(req_data, Message.PRESERVE_ORDER)
             if self.__request_msg.get_opcode() != Opcode.UPDATE():
             if self.__request_msg.get_opcode() != Opcode.UPDATE():
-                raise SessionError('Update request has unexpected opcode: ' +
-                                   str(self.__request_msg.get_opcode()))
+                raise InternalError('Update request has unexpected opcode: ' +
+                                    str(self.__request_msg.get_opcode()))
             tsig_ctx = self.__check_request_tsig(self.__request_msg, req_data)
             tsig_ctx = self.__check_request_tsig(self.__request_msg, req_data)
         except Exception as ex:
         except Exception as ex:
             logger.error(DDNS_REQUEST_PARSE_FAIL, ex)
             logger.error(DDNS_REQUEST_PARSE_FAIL, ex)
@@ -372,8 +376,11 @@ class DDNSServer:
         else:
         else:
             msg.to_wire(self.__response_renderer)
             msg.to_wire(self.__response_renderer)
 
 
-        return self.__send_response(sock, self.__response_renderer.get_data(),
-                                    remote_addr)
+        ret = self.__send_response(sock, self.__response_renderer.get_data(),
+                                   remote_addr)
+        if result == isc.ddns.session.UPDATE_SUCCESS:
+            self.__notify_update(zname, zclass)
+        return ret
 
 
     def __send_response(self, sock, data, dest):
     def __send_response(self, sock, data, dest):
         '''Send DDNS response to the client.
         '''Send DDNS response to the client.
@@ -401,6 +408,42 @@ class DDNSServer:
 
 
         return True
         return True
 
 
+    def __notify_update(self, zname, zclass):
+        '''Notify other modules of the update.
+
+        Note that we use blocking communication here.  While the internal
+        communication bus is generally expected to be pretty responsive and
+        error free, notable delay can still occur, and in worse cases timeouts
+        or connection reset can happen.  In these cases, even if the trouble
+        is temporary, the update service will be suspended for a while.
+        For a longer term we'll need to switch to asynchronous communication,
+        but for now we rely on the blocking operation.
+
+        Note also that we directly refer to the "protected" member of
+        ccsession (_cc._session) rather than creating a separate channel.
+        It's probably not the best practice, but hopefully we can introduce
+        a cleaner way when we support asynchronous communication.
+        At the moment we prefer the brevity with the use of internal channel
+        of the cc session.
+
+        '''
+        param = {'zone_name': zname.to_text(), 'zone_class': zclass.to_text()}
+        msg = create_command(ZONE_NEW_DATA_READY_CMD, param)
+        modname = XFROUT_MODULE_NAME
+        try:
+            seq = self._cc._session.group_sendmsg(msg, modname)
+            answer, _ = self._cc._session.group_recvmsg(False, seq)
+            rcode, error_msg = parse_answer(answer)
+        except (SessionTimeout, SessionError, ProtocolError) as ex:
+            rcode = 1
+            error_msg = str(ex)
+        if rcode == 0:
+            logger.debug(TRACE_BASIC, DDNS_UPDATE_NOTIFY, modname,
+                         ZoneFormatter(zname, zclass))
+        else:
+            logger.error(DDNS_UPDATE_NOTIFY_FAIL, modname,
+                         ZoneFormatter(zname, zclass), error_msg)
+
     def handle_session(self, fileno):
     def handle_session(self, fileno):
         """
         """
         Handle incoming session on the socket with given fileno.
         Handle incoming session on the socket with given fileno.

+ 23 - 0
src/bin/ddns/ddns_messages.mes

@@ -112,3 +112,26 @@ process will now shut down.
 The b10-ddns process encountered an uncaught exception and will now shut
 The b10-ddns process encountered an uncaught exception and will now shut
 down. This is indicative of a programming error and should not happen under
 down. This is indicative of a programming error and should not happen under
 normal circumstances. The exception type and message are printed.
 normal circumstances. The exception type and message are printed.
+
+% DDNS_UPDATE_NOTIFY_FAIL failed to notify %1 of updates to %2: %3
+b10-ddns has made updates to a zone based on an update request and
+tried to notify an external module of the updates, but the
+notification fails.  Severity of this effect depends on the type of
+the module.  If it's b10-xfrout, this means DNS notify messages won't
+be sent to secondary servers of the zone.  It's suboptimal, but not
+necessarily critical as the secondary servers will try to check the
+zone's status periodically.  If it's b10-auth and the notification was
+needed to have it reload the corresponding zone, it's more serious
+because b10-auth won't be able to serve the new version of the zone
+unless some explicit recovery action is taken.  So the administrator
+needs to examine this message and takes an appropriate action.  In
+either case, this notification is generally expected to succeed; so
+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.
+
+% DDNS_UPDATE_NOTIFY notified %1 of updates to %2
+Debug message.  b10-ddns has made updates to a zone based on an update
+request and has successfully notified an external module of the updates.
+The notified module will use that information for updating its own
+state or any necessary protocol action such as zone reloading or sending
+notify messages to secondary servers.

+ 113 - 3
src/bin/ddns/tests/ddns_test.py

@@ -19,7 +19,9 @@ from isc.ddns.session import *
 from isc.dns import *
 from isc.dns import *
 from isc.acl.acl import ACCEPT
 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.datasrc import DataSourceClient
 from isc.datasrc import DataSourceClient
+from isc.config.ccsession import create_answer
 import ddns
 import ddns
 import errno
 import errno
 import os
 import os
@@ -147,6 +149,10 @@ class FakeKeyringModule:
 
 
 class MyCCSession(isc.config.ConfigData):
 class MyCCSession(isc.config.ConfigData):
     '''Fake session with minimal interface compliance.'''
     '''Fake session with minimal interface compliance.'''
+
+    # faked CC sequence used in group_send/recvmsg
+    FAKE_SEQUENCE = 53
+
     def __init__(self):
     def __init__(self):
         module_spec = isc.config.module_spec_from_file(
         module_spec = isc.config.module_spec_from_file(
             ddns.SPECFILE_LOCATION)
             ddns.SPECFILE_LOCATION)
@@ -155,6 +161,14 @@ class MyCCSession(isc.config.ConfigData):
         self._stopped = False
         self._stopped = False
         # Used as the return value of get_remote_config_value.  Customizable.
         # Used as the return value of get_remote_config_value.  Customizable.
         self.auth_db_file = READ_ZONE_DB_FILE
         self.auth_db_file = READ_ZONE_DB_FILE
+        # faked cc channel, providing group_send/recvmsg itself.  The following
+        # attributes are for inspection/customization in tests.
+        self._session = self
+        self._sent_msg = []
+        self._recvmsg_called = 0
+        self._answer_code = 0   # code used in answer returned via recvmsg
+        self._sendmsg_exception = None # will be raised from sendmsg if !None
+        self._recvmsg_exception = None # will be raised from recvmsg if !None
 
 
     def start(self):
     def start(self):
         '''Called by DDNSServer initialization, but not used in tests'''
         '''Called by DDNSServer initialization, but not used in tests'''
@@ -177,6 +191,32 @@ class MyCCSession(isc.config.ConfigData):
         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
 
 
+    def group_sendmsg(self, msg, group):
+        # remember the passed parameter, and return dummy sequence
+        self._sent_msg.append((msg, group))
+        if self._sendmsg_exception is not None:
+            raise self._sendmsg_exception
+        return self.FAKE_SEQUENCE
+
+    def group_recvmsg(self, nonblock, seq):
+        self._recvmsg_called += 1
+        if seq != self.FAKE_SEQUENCE:
+            raise RuntimeError('unexpected CC sequence: ' + str(seq))
+        if self._recvmsg_exception is not None:
+            raise self._recvmsg_exception
+        if self._answer_code is 0:
+            return create_answer(0), None
+        else:
+            return create_answer(self._answer_code, "dummy error value"), None
+
+    def clear_msg(self):
+        '''Clear instrumental attributes related session messages.'''
+        self._sent_msg = []
+        self._recvmsg_called = 0
+        self._answer_code = 0
+        self._sendmsg_exception = None
+        self._recvmsg_exception = None
+
 class MyDDNSServer():
 class MyDDNSServer():
     '''Fake DDNS server used to test the main() function'''
     '''Fake DDNS server used to test the main() function'''
     def __init__(self):
     def __init__(self):
@@ -573,11 +613,11 @@ def create_msg(opcode=Opcode.UPDATE(), zones=[TEST_ZONE_RECORD], prereq=[],
 
 
 class TestDDNSession(unittest.TestCase):
 class TestDDNSession(unittest.TestCase):
     def setUp(self):
     def setUp(self):
-        cc_session = MyCCSession()
-        self.assertFalse(cc_session._started)
+        self.__cc_session = MyCCSession()
+        self.assertFalse(self.__cc_session._started)
         self.orig_tsig_keyring = isc.server_common.tsig_keyring
         self.orig_tsig_keyring = isc.server_common.tsig_keyring
         isc.server_common.tsig_keyring = FakeKeyringModule()
         isc.server_common.tsig_keyring = FakeKeyringModule()
-        self.server = ddns.DDNSServer(cc_session)
+        self.server = ddns.DDNSServer(self.__cc_session)
         self.server._UpdateSessionClass = self.__fake_session_creator
         self.server._UpdateSessionClass = self.__fake_session_creator
         self.__faked_result = UPDATE_SUCCESS # will be returned by fake session
         self.__faked_result = UPDATE_SUCCESS # will be returned by fake session
         self.__sock = FakeSocket(-1)
         self.__sock = FakeSocket(-1)
@@ -705,6 +745,76 @@ class TestDDNSession(unittest.TestCase):
         num_rrsets = len(self.__req_message.get_section(SECTION_PREREQUISITE))
         num_rrsets = len(self.__req_message.get_section(SECTION_PREREQUISITE))
         self.assertEqual(2, num_rrsets)
         self.assertEqual(2, num_rrsets)
 
 
+    def check_session_msg(self, result, expect_recv=1):
+        '''Check post update communication with other modules.'''
+        # iff the update succeeds, b10-ddns should tell interested other
+        # modules the information about the update zone in the form of
+        # {'command': ['notify', {'zone_name': <updated_zone_name>,
+        #                         'zone_class', <updated_zone_class>}]}
+        # and expect an answer by calling group_recvmsg().
+        #
+        # expect_recv indicates the expected number of calls to
+        # group_recvmsg(), which is normally 1, but can be 0 if send fails.
+        if result == UPDATE_SUCCESS:
+            self.assertEqual(1, len(self.__cc_session._sent_msg))
+            self.assertEqual(expect_recv, self.__cc_session._recvmsg_called)
+            sent_msg, sent_group = self.__cc_session._sent_msg[0]
+            sent_cmd = sent_msg['command']
+            self.assertEqual('Xfrout', sent_group)
+            self.assertEqual('notify', sent_cmd[0])
+            self.assertEqual(2, len(sent_cmd[1]))
+            self.assertEqual(TEST_ZONE_NAME.to_text(), sent_cmd[1]['zone_name'])
+            self.assertEqual(TEST_RRCLASS.to_text(), sent_cmd[1]['zone_class'])
+        else:
+            # for other result cases neither send nor recvmsg should be called.
+            self.assertEqual([], self.__cc_session._sent_msg)
+            self.assertEqual(0, self.__cc_session._recvmsg_called)
+
+    def test_session_msg(self):
+        '''Test post update communication with other modules.'''
+        # Normal cases, confirming communication takes place iff update
+        # succeeds
+        for r in [UPDATE_SUCCESS, UPDATE_ERROR, UPDATE_DROP]:
+            self.__cc_session.clear_msg()
+            self.check_session(result=r)
+            self.check_session_msg(r)
+
+        # Return an error from the remote module, which should be just ignored.
+        self.__cc_session.clear_msg()
+        self.__cc_session._answer_code = 1
+        self.check_session()
+        self.check_session_msg(UPDATE_SUCCESS)
+
+        # raise some exceptions from the faked session.  Expected ones are
+        # simply (logged and) ignored
+        self.__cc_session.clear_msg()
+        self.__cc_session._recvmsg_exception = SessionTimeout('dummy timeout')
+        self.check_session()
+        self.check_session_msg(UPDATE_SUCCESS)
+
+        self.__cc_session.clear_msg()
+        self.__cc_session._recvmsg_exception = SessionError('dummy error')
+        self.check_session()
+        self.check_session_msg(UPDATE_SUCCESS)
+
+        self.__cc_session.clear_msg()
+        self.__cc_session._recvmsg_exception = ProtocolError('dummy perror')
+        self.check_session()
+        self.check_session_msg(UPDATE_SUCCESS)
+
+        # Similar to the previous cases, but sendmsg() raises, so there should
+        # be no call to recvmsg().
+        self.__cc_session.clear_msg()
+        self.__cc_session._sendmsg_exception = SessionError('send error')
+        self.check_session()
+        self.check_session_msg(UPDATE_SUCCESS, expect_recv=0)
+
+        # Unexpected exception will be propagated (and will terminate the
+        # server)
+        self.__cc_session.clear_msg()
+        self.__cc_session._sendmsg_exception = RuntimeError('unexpected')
+        self.assertRaises(RuntimeError, self.check_session)
+
     def test_session_with_config(self):
     def test_session_with_config(self):
         '''Check a session with more relistic config setups
         '''Check a session with more relistic config setups