Browse Source

[master] Merge branch 'trac2712'

JINMEI Tatuya 12 years ago
parent
commit
fa392e8eb3

+ 45 - 21
src/bin/cmdctl/cmdctl.py.in

@@ -247,6 +247,7 @@ class CommandControl():
         CommandControl to communicate with other modules. '''
         CommandControl to communicate with other modules. '''
         self._verbose = verbose
         self._verbose = verbose
         self._httpserver = httpserver
         self._httpserver = httpserver
+        self.__msg_handler_thread = None # set in _start_msg_handle_thread
         self._lock = threading.Lock()
         self._lock = threading.Lock()
         self._setup_session()
         self._setup_session()
         self.modules_spec = self._get_modules_specification()
         self.modules_spec = self._get_modules_specification()
@@ -326,7 +327,26 @@ class CommandControl():
                     self._cmdctl_config_data[key] = new_config[key]
                     self._cmdctl_config_data[key] = new_config[key]
         return answer
         return answer
 
 
+    def _get_current_thread(self):
+        """A simple wrapper of returning the 'current' thread object.
+
+        This is extracted as a 'protected' method so tests can override for
+        their convenience.
+
+        """
+        return threading.currentThread()
+
     def command_handler(self, command, args):
     def command_handler(self, command, args):
+        """Handle commands from other modules.
+
+        This method must not be called by any other threads than
+        __msg_handler_thread invoked at the intialization of the class;
+        otherwise it would cause critical race or dead locks.
+
+        """
+        # Check the restriction described above.
+        assert self._get_current_thread() == self.__msg_handler_thread
+
         answer = ccsession.create_answer(0)
         answer = ccsession.create_answer(0)
         if command == ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE:
         if command == ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE:
             # The 'value' of a specification update can be either
             # The 'value' of a specification update can be either
@@ -362,6 +382,7 @@ class CommandControl():
         ''' Start one thread to handle received message from msgq.'''
         ''' Start one thread to handle received message from msgq.'''
         td = threading.Thread(target=self._handle_msg_from_msgq)
         td = threading.Thread(target=self._handle_msg_from_msgq)
         td.daemon = True
         td.daemon = True
+        self.__msg_handler_thread = td
         td.start()
         td.start()
 
 
     def _handle_msg_from_msgq(self):
     def _handle_msg_from_msgq(self):
@@ -402,7 +423,7 @@ class CommandControl():
         rcode, reply = self.send_command('ConfigManager', ccsession.COMMAND_GET_MODULE_SPEC)
         rcode, reply = self.send_command('ConfigManager', ccsession.COMMAND_GET_MODULE_SPEC)
         return self._parse_command_result(rcode, reply)
         return self._parse_command_result(rcode, reply)
 
 
-    def send_command_with_check(self, module_name, command_name, params = None):
+    def send_command_with_check(self, module_name, command_name, params=None):
         '''Before send the command to modules, check if module_name, command_name
         '''Before send the command to modules, check if module_name, command_name
         parameters are legal according the spec file of the module.
         parameters are legal according the spec file of the module.
         Return rcode, dict. TODO, the rcode should be defined properly.
         Return rcode, dict. TODO, the rcode should be defined properly.
@@ -424,31 +445,34 @@ class CommandControl():
 
 
         return self.send_command(module_name, command_name, params)
         return self.send_command(module_name, command_name, params)
 
 
-    def send_command(self, module_name, command_name, params = None):
-        '''Send the command from bindctl to proper module. '''
+    def send_command(self, module_name, command_name, params=None):
+        """Send the command from bindctl to proper module.
+
+        Note that commands sent to Cmdctl itself are also delivered via the
+        CC session.  Since this method is called from a thread handling a
+        particular HTTP session, it cannot directly call command_handler().
+
+        """
         errstr = 'unknown error'
         errstr = 'unknown error'
         answer = None
         answer = None
         logger.debug(DBG_CMDCTL_MESSAGING, CMDCTL_SEND_COMMAND,
         logger.debug(DBG_CMDCTL_MESSAGING, CMDCTL_SEND_COMMAND,
                      command_name, module_name)
                      command_name, module_name)
 
 
-        if module_name == self._module_name:
-            # Process the command sent to cmdctl directly.
-            answer = self.command_handler(command_name, params)
-        else:
-            # FIXME: Due to the fact that we use a separate session
-            # from the module one (due to threads and blocking), and
-            # because the plain cc session does not have the high-level
-            # rpc-call method, we use the low-level way and create the
-            # command ourselves.
-            msg = ccsession.create_command(command_name, params)
-            seq = self._cc.group_sendmsg(msg, module_name, want_answer=True)
-            logger.debug(DBG_CMDCTL_MESSAGING, CMDCTL_COMMAND_SENT,
-                         command_name, module_name)
-            #TODO, it may be blocked, msqg need to add a new interface waiting in timeout.
-            try:
-                answer, env = self._cc.group_recvmsg(False, seq)
-            except isc.cc.session.SessionTimeout:
-                errstr = "Module '%s' not responding" % module_name
+        # FIXME: Due to the fact that we use a separate session
+        # from the module one (due to threads and blocking), and
+        # because the plain cc session does not have the high-level
+        # rpc-call method, we use the low-level way and create the
+        # command ourselves.
+        msg = ccsession.create_command(command_name, params)
+        seq = self._cc.group_sendmsg(msg, module_name, want_answer=True)
+        logger.debug(DBG_CMDCTL_MESSAGING, CMDCTL_COMMAND_SENT, command_name,
+                     module_name)
+        # TODO, it may be blocked, msqg need to add a new interface waiting
+        # in timeout.
+        try:
+            answer, env = self._cc.group_recvmsg(False, seq)
+        except isc.cc.session.SessionTimeout:
+            errstr = "Module '%s' not responding" % module_name
 
 
         if answer:
         if answer:
             try:
             try:

+ 47 - 2
src/bin/cmdctl/tests/cmdctl_test.py

@@ -373,7 +373,30 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
             self.handler._is_user_logged_in = orig_is_user_logged_in
             self.handler._is_user_logged_in = orig_is_user_logged_in
             self.handler._check_user_name_and_pwd = orig_check_user_name_and_pwd
             self.handler._check_user_name_and_pwd = orig_check_user_name_and_pwd
 
 
+class MockSession:
+    """Act like isc.cc.Session, stealing group_sendmsg/recvmsg().
+
+    The initial simple version only records given parameters in
+    group_sendmsg() for later inspection and raise a timeout exception
+    from recvmsg().  As we see the need for more test cases these methods
+    should be extended.
+
+    """
+    def __init__(self, sent_messages):
+        self.__sent_messages = sent_messages
+
+    def group_sendmsg(self, msg, module_name, want_answer):
+        self.__sent_messages.append((msg, module_name))
+
+    def group_recvmsg(self, nonblock, seq):
+        raise isc.cc.session.SessionTimeout('dummy timeout')
+
 class MyCommandControl(CommandControl):
 class MyCommandControl(CommandControl):
+    def __init__(self, httpserver, verbose):
+        super().__init__(httpserver, verbose)
+        self.sent_messages = [] # for inspection; allow tests to see it
+        self._cc = MockSession(self.sent_messages)
+
     def _get_modules_specification(self):
     def _get_modules_specification(self):
         return {}
         return {}
 
 
@@ -390,6 +413,12 @@ class MyCommandControl(CommandControl):
     def _handle_msg_from_msgq(self):
     def _handle_msg_from_msgq(self):
         pass
         pass
 
 
+    def _start_msg_handle_thread(self): # just not bother to be threads
+        pass
+
+    def _get_current_thread(self):
+        return None
+
 class TestCommandControl(unittest.TestCase):
 class TestCommandControl(unittest.TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -502,8 +531,24 @@ class TestCommandControl(unittest.TestCase):
         os.remove(file_name)
         os.remove(file_name)
 
 
     def test_send_command(self):
     def test_send_command(self):
-        rcode, value = self.cmdctl.send_command('Cmdctl', 'print_settings', None)
-        self.assertEqual(rcode, 0)
+        # Send a command to other module.  We check an expected message
+        # is sent via the session (cmdct._cc).  Due to the behavior of
+        # our mock session object the anser will be "fail", but it's not
+        # the subject of this test, and so it's okay.
+        # TODO: more detailed cases should be tested.
+        rcode, value = self.cmdctl.send_command('Init', 'shutdown', None)
+        self.assertEqual(1, len(self.cmdctl.sent_messages))
+        self.assertEqual(({'command': ['shutdown']}, 'Init'),
+                         self.cmdctl.sent_messages[-1])
+        self.assertEqual(1, rcode)
+
+        # Send a command to cmdctl itself.  Should be the same effect.
+        rcode, value = self.cmdctl.send_command('Cmdctl', 'print_settings',
+                                                None)
+        self.assertEqual(2, len(self.cmdctl.sent_messages))
+        self.assertEqual(({'command': ['print_settings']}, 'Cmdctl'),
+                         self.cmdctl.sent_messages[-1])
+        self.assertEqual(1, rcode)
 
 
 class MySecureHTTPServer(SecureHTTPServer):
 class MySecureHTTPServer(SecureHTTPServer):
     def server_bind(self):
     def server_bind(self):

+ 15 - 0
tests/lettuce/features/bindctl_commands.feature

@@ -154,3 +154,18 @@ Feature: control with bindctl
         bind10 module Xfrout should be running
         bind10 module Xfrout should be running
         bind10 module Xfrin should be running
         bind10 module Xfrin should be running
         bind10 module Zonemgr should be running
         bind10 module Zonemgr should be running
+
+    Scenario: Shutting down a certain module
+        # We could test with several modules, but for now we are particularly
+        # interested in shutting down cmdctl.  It previously caused hangup,
+        # so this scenario confirms it's certainly fixed.  Note: since cmdctl
+        # is a "needed" component, shutting it down will result in system
+        # shutdown.  So "send bind10 command" will fail (it cannot complete
+        # "quit").
+        Given I have bind10 running with configuration bindctl/bindctl.config
+        And wait for bind10 stderr message BIND10_STARTED_CC
+        And wait for bind10 stderr message CMDCTL_STARTED
+
+        When I send bind10 ignoring failure the command Cmdctl shutdown
+        And wait for bind10 stderr message CMDCTL_EXITING
+        And wait for bind10 stderr message BIND10_SHUTDOWN_COMPLETE

+ 15 - 5
tests/lettuce/features/terrain/bind10_control.py

@@ -120,13 +120,15 @@ def have_bind10_running(step, config_file, cmdctl_port, process_name):
     step.given(start_step)
     step.given(start_step)
 
 
 # function to send lines to bindctl, and store the result
 # function to send lines to bindctl, and store the result
-def run_bindctl(commands, cmdctl_port=None):
+def run_bindctl(commands, cmdctl_port=None, ignore_failure=False):
     """Run bindctl.
     """Run bindctl.
        Parameters:
        Parameters:
        commands: a sequence of strings which will be sent.
        commands: a sequence of strings which will be sent.
        cmdctl_port: a port number on which cmdctl is listening, is converted
        cmdctl_port: a port number on which cmdctl is listening, is converted
                     to string if necessary. If not provided, or None, defaults
                     to string if necessary. If not provided, or None, defaults
                     to 47805
                     to 47805
+       ignore_failure(bool): if set to True, don't examin the result code
+                    of bindctl and assert it succeeds.
 
 
        bindctl's stdout and stderr streams are stored (as one multiline string
        bindctl's stdout and stderr streams are stored (as one multiline string
        in world.last_bindctl_stdout/stderr.
        in world.last_bindctl_stdout/stderr.
@@ -140,6 +142,8 @@ def run_bindctl(commands, cmdctl_port=None):
     for line in commands:
     for line in commands:
         bindctl.stdin.write(line + "\n")
         bindctl.stdin.write(line + "\n")
     (stdout, stderr) = bindctl.communicate()
     (stdout, stderr) = bindctl.communicate()
+    if ignore_failure:
+        return
     result = bindctl.returncode
     result = bindctl.returncode
     world.last_bindctl_stdout = stdout
     world.last_bindctl_stdout = stdout
     world.last_bindctl_stderr = stderr
     world.last_bindctl_stderr = stderr
@@ -306,19 +310,25 @@ def config_remove_command(step, name, value, cmdctl_port):
                 "quit"]
                 "quit"]
     run_bindctl(commands, cmdctl_port)
     run_bindctl(commands, cmdctl_port)
 
 
-@step('send bind10(?: with cmdctl port (\d+))? the command (.+)')
-def send_command(step, cmdctl_port, command):
+@step('send bind10(?: with cmdctl port (\d+))?( ignoring failure)? the command (.+)')
+def send_command(step, cmdctl_port, ignore_failure, command):
     """
     """
     Run bindctl, send the given command, and exit bindctl.
     Run bindctl, send the given command, and exit bindctl.
     Parameters:
     Parameters:
     command ('the command <command>'): The command to send.
     command ('the command <command>'): The command to send.
     cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
     cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
                 the command to. Defaults to 47805.
                 the command to. Defaults to 47805.
-    Fails if cmdctl does not exit with status code 0.
+    ignore_failure ('ignoring failure', optional): set to None if bindctl
+    is expected to succeed (normal case, which is the default); if it is
+    not None, it means bindctl is expected to fail (and it's acceptable).
+
+    Fails if bindctl does not exit with status code 0 and ignore_failure
+    is not None.
+
     """
     """
     commands = [command,
     commands = [command,
                 "quit"]
                 "quit"]
-    run_bindctl(commands, cmdctl_port)
+    run_bindctl(commands, cmdctl_port, ignore_failure is not None)
 
 
 @step('bind10 module (\S+) should( not)? be running')
 @step('bind10 module (\S+) should( not)? be running')
 def module_is_running(step, name, not_str):
 def module_is_running(step, name, not_str):