BIND 10 master, updated. 230ccceb4112042ce3aadc7cfd6c9780afa23bbb [master] Clear components dict as documented in BoB.kill_started_components()

BIND 10 source code commits bind10-changes at lists.isc.org
Tue Dec 11 03:06:58 UTC 2012


The branch, master has been updated
       via  230ccceb4112042ce3aadc7cfd6c9780afa23bbb (commit)
       via  c0be62cb0f27a0855c007beabeb0c3ba3c4fb0ff (commit)
       via  3e2d8b00ba86773f3cba03f935e5847cee109f84 (commit)
       via  32011f9a367b479a983dbb32e877590b18fbc275 (commit)
       via  02f306c854846cf972886c8f42e1b6e642c89e4a (commit)
       via  6af3de2d2ee4b43bb5056d8b11cba164ac693346 (commit)
       via  366f210b8217811fb1cd96ed996f3491c7d9766f (commit)
       via  e6ebe1de8e6ab2982869ba27f1719e63fddeb0e3 (commit)
       via  de66fdd73b8f545768166b7406be878c137cc019 (commit)
       via  cdbe6dedf437b052cb60777c470cfda312f50b43 (commit)
       via  029ac581f439485d797d195914f52a60eb593a60 (commit)
       via  e8a23d31110a2824368bff0c41f79016d696f613 (commit)
       via  f7e301237c7806b582172fcf80a892659a3d9e32 (commit)
       via  4f806512c8b2a99f3b1dc43fda7130ec81d21c1f (commit)
       via  87cd15b3cc34738753d97e62a07681b245350f25 (commit)
       via  3c17e9586c94a0e4331cafe79e379e71f3d8a561 (commit)
       via  bae03c0fbf0a82c2b88cb683fc81c30ab7f70ad0 (commit)
       via  ec4581da1a15031128504cf97c8c578669422126 (commit)
       via  70db0c50bf842765d90e2a6b3abf4f5360f31281 (commit)
       via  609e5bf4005731160716d1d63d3eebf402a14f1d (commit)
       via  71cf0a22ba08d32908a610b749e9aefe1c4dda1a (commit)
       via  40d54f7e2b04dc53be81a24e50f8e66be968e6cb (commit)
       via  30e529ac56321e0654bf8bbe48412b851bd06bb9 (commit)
       via  f1035ba18d68d4a1fa89719a9dc8e5eb6ae24d84 (commit)
       via  0740a5d8df7f65518b2b87662b4375630e1838ef (commit)
       via  045d32e4a9f14e7deec58e5e7d55867a34325764 (commit)
       via  eba06eeedde4091fed26761f696a2aab5535074a (commit)
       via  93d27cba1a47182b5f94e45b5d569386f2a1ad25 (commit)
       via  8a966b8d7359a852478e380e89e94cfe2f12d5bd (commit)
       via  2c2459fd09fd1fb9b2c741eca0b93a8550f3f9b3 (commit)
       via  145a4ed5a89c92d93303604ceff83ec3d17ff87d (commit)
       via  41ccdad87bfc96af06dd83ecd143e4277795eb5f (commit)
       via  8682cdcb920f12df639aaa0e7720afb9b03bf849 (commit)
       via  0688fdb85ded658b2ecf5aceb6e4691cb921ddfb (commit)
       via  cfdfb58ce442816cce069453d4051cc008691b74 (commit)
       via  08a5a3d31220c660530bd69cccc8c27e1a789312 (commit)
       via  7f6ecb5e40c6acede82fb08a7d4c477030a48b99 (commit)
       via  f6c776bc4a3cea0eae628d1580ab10843be06dfe (commit)
       via  bc4926dac23a819a909992abf58df64d62647c68 (commit)
       via  fa3e24bb027671b5c53a5fa1b004920cf8252f0f (commit)
       via  a11abf30b29eda75f8680c8287b57db1abe612e8 (commit)
       via  996d6ff3b0e79c723332d03296cbbe88b552d373 (commit)
       via  924c7a481efc5aa34c8bc73efb395c2c80282cc0 (commit)
       via  8cb9e9efb9cd47301e6784840142931631e1664f (commit)
       via  f117a66b8be41e88a18a27bd7b2f1275b624767a (commit)
       via  77c012c47a2bd2ff161b8b62df065b3daa87ff73 (commit)
       via  d51421238cd7aa392c6baa967ee977a6d3dc8b7a (commit)
       via  a9ebc379b9d2f2501c8a4d497bb715f6c100cea8 (commit)
       via  1ab0c5acf4fa6b8a1a99068685bb2a27d6dd3dc8 (commit)
       via  15249e567c9cfae135c1a2bc1a110b4a635b11ab (commit)
       via  da089cd9b9dc3aa3cb13f1c02e362b870e524b20 (commit)
       via  5450b258dbeaaf876fd35c52a1191e4f083a620e (commit)
       via  b1c8e1ce93d760fa32e7b977910faa9ed5fc4c30 (commit)
       via  58f0aac3a8f36f335c0546f44122ff43f42f5761 (commit)
       via  84fbdaeddb176af0a5cd73caf76698208b4e94a7 (commit)
       via  09f21f49fa1e214af117262c305efb3968becd8f (commit)
       via  36d2b848c6badf00bae61bd958b891b7e2acc8cd (commit)
       via  4f52e898a7b69228cf4a8ab70d78844ad9dc45e1 (commit)
       via  c2c64eae1f3e6140bbe30e963797b9a4efa5a9f8 (commit)
       via  230de14b1b3e31b2f436cfd370e149a98926522e (commit)
       via  54a707d39ae1943731ed2043f8d44424c434f0e3 (commit)
       via  51bd304db036e8d2e6008a98b2dae01eea126e41 (commit)
       via  da1b2d7dc0a367ed168b366df1ab5c5af82260d4 (commit)
       via  d5c0c97ef8bf7d4b9ae2709b8a92eb1532f4c9b9 (commit)
       via  a7b31b7b074db8359b5c93c3b0d37c807a78ad7e (commit)
       via  7503458897ccad85875ef3bc929ec803750179e8 (commit)
       via  37da25f2eca6b2af80cc5bc2a7e65c5644acf2fb (commit)
       via  0f228f2d53a626d647aa728585342c889ff8f846 (commit)
       via  8b31b93c4c62b76b3ec70d0e9c1a18c0f51bd459 (commit)
       via  f9445ffa2d128ab0730abb3592e75475023ba755 (commit)
       via  494bbca16cea584ba6ad5d2c5f9a273d809f902d (commit)
       via  67f4798a8a2457b8d284b75dcf1a6bcfeb8a0fa5 (commit)
       via  32e88854b4ab9b2484d1f5ad0597869c8d1f5da4 (commit)
       via  9d23c50d12b04b51c10889fe9b476823550bd733 (commit)
       via  83d4f414a296dfc757a7a4f6e8c8fcf7bd24ca2e (commit)
       via  424b8d83f6059738f2edb01564ec9ce1db223b08 (commit)
       via  3590bad22f52396c91663cc75b8548f6df134c60 (commit)
       via  4a9b794019090fd826688bca0aee4dc565756cc1 (commit)
       via  180a437742219f6266934738962663c65ab748f6 (commit)
       via  6210dae2a6749d8c1e328de433c91058fd54b756 (commit)
       via  6b8c62acd872220aeb061f7c6e0fe50348ba3d60 (commit)
       via  b0db66ca291884dd9220cda078d9ad3e91d0eb67 (commit)
       via  71739287e490437d2f1278e02ba9232afa925a48 (commit)
       via  7ba306a2e14dd31e1bf8350c2d27b9741af7e101 (commit)
       via  9b6c74f8e85b29db87fb4724698d9fb9cc53dcc4 (commit)
       via  356a8b2e2cdbd2470eb134bfe08f80fb54028abb (commit)
      from  172e704b668189d6057c4bfc80403edd59aadd5c (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 230ccceb4112042ce3aadc7cfd6c9780afa23bbb
Author: Mukund Sivaraman <muks at isc.org>
Date:   Tue Dec 11 06:56:57 2012 +0530

    [master] Clear components dict as documented in BoB.kill_started_components()
    
    This issue was detected post-merge, and seems to have been introduced
    by commit 86ed7ae9cfb4184f5637a2e478242f0a646ba2e1 in master.

commit c0be62cb0f27a0855c007beabeb0c3ba3c4fb0ff
Merge: 3e2d8b0 172e704
Author: Mukund Sivaraman <muks at isc.org>
Date:   Tue Dec 11 05:54:01 2012 +0530

    Merge branch 'master' into trac2353
    
    Conflicts:
    	src/bin/bind10/bind10_src.py.in

-----------------------------------------------------------------------

Summary of changes:
 src/bin/bind10/bind10_src.py.in        |   53 ++-
 src/bin/bind10/tests/bind10_test.py.in |  765 +++++++++++++++++++++++++++++++-
 2 files changed, 802 insertions(+), 16 deletions(-)

-----------------------------------------------------------------------
diff --git a/src/bin/bind10/bind10_src.py.in b/src/bin/bind10/bind10_src.py.in
index 36ad760..9c989d9 100755
--- a/src/bin/bind10/bind10_src.py.in
+++ b/src/bin/bind10/bind10_src.py.in
@@ -216,6 +216,12 @@ class BoB:
         self.clear_config = clear_config
         self.cmdctl_port = cmdctl_port
         self.wait_time = wait_time
+        self.msgq_timeout = 5
+
+        # _run_under_unittests is only meant to be used when testing. It
+        # bypasses execution of some code to help with testing.
+        self._run_under_unittests = False
+
         self._component_configurator = isc.bind10.component.Configurator(self,
             isc.bind10.special_component.get_specials())
         # The priorities here make them start in the correct order. First
@@ -332,6 +338,7 @@ class BoB:
         """
         logger.info(BIND10_KILLING_ALL_PROCESSES)
         self.__kill_children(True)
+        self.components = {}
 
     def _read_bind10_config(self):
         """
@@ -408,21 +415,34 @@ class BoB:
     # raised which is caught by the caller of start_all_processes(); this kills
     # processes started up to that point before terminating the program.
 
+    def _make_process_info(self, name, args, env,
+                           dev_null_stdout=False, dev_null_stderr=False):
+        """
+            Wrapper around ProcessInfo(), useful to override
+            ProcessInfo() creation during testing.
+        """
+        return ProcessInfo(name, args, env, dev_null_stdout, dev_null_stderr)
+
     def start_msgq(self):
         """
             Start the message queue and connect to the command channel.
         """
         self.log_starting("b10-msgq")
-        msgq_proc = ProcessInfo("b10-msgq", ["b10-msgq"], self.c_channel_env,
-                                True, not self.verbose)
+        msgq_proc = self._make_process_info("b10-msgq", ["b10-msgq"],
+                                            self.c_channel_env,
+                                            True, not self.verbose)
         msgq_proc.spawn()
         self.log_started(msgq_proc.pid)
 
         # Now connect to the c-channel
         cc_connect_start = time.time()
         while self.cc_session is None:
+            # if we are run under unittests, break
+            if self._run_under_unittests:
+                break
+
             # if we have been trying for "a while" give up
-            if (time.time() - cc_connect_start) > 5:
+            if (time.time() - cc_connect_start) > self.msgq_timeout:
                 logger.error(BIND10_CONNECTING_TO_CC_FAIL)
                 raise CChannelConnectError("Unable to connect to c-channel after 5 seconds")
 
@@ -434,7 +454,8 @@ class BoB:
 
         # Subscribe to the message queue.  The only messages we expect to receive
         # on this channel are once relating to process startup.
-        self.cc_session.group_subscribe("Boss")
+        if self.cc_session is not None:
+            self.cc_session.group_subscribe("Boss")
 
         return msgq_proc
 
@@ -450,13 +471,14 @@ class BoB:
             args.append("--config-filename=" + self.config_filename)
         if self.clear_config:
             args.append("--clear-config")
-        bind_cfgd = ProcessInfo("b10-cfgmgr", args,
-                                self.c_channel_env)
+        bind_cfgd = self._make_process_info("b10-cfgmgr", args,
+                                            self.c_channel_env)
         bind_cfgd.spawn()
         self.log_started(bind_cfgd.pid)
 
-        # Wait for the configuration manager to start up as subsequent initialization
-        # cannot proceed without it.  The time to wait can be set on the command line.
+        # Wait for the configuration manager to start up as
+        # subsequent initialization cannot proceed without it.  The
+        # time to wait can be set on the command line.
         time_remaining = self.wait_time
         msg, env = self.cc_session.group_recvmsg()
         while time_remaining > 0 and not self.process_running(msg, "ConfigManager"):
@@ -499,7 +521,7 @@ class BoB:
             The port and address arguments are for log messages only.
         """
         self.log_starting(name, port, address)
-        newproc = ProcessInfo(name, args, c_channel_env)
+        newproc = self._make_process_info(name, args, c_channel_env)
         newproc.spawn()
         self.log_started(newproc.pid)
         return newproc
@@ -611,7 +633,6 @@ class BoB:
         if self.msgq_socket_file is not None:
              c_channel_env["BIND10_MSGQ_SOCKET_FILE"] = self.msgq_socket_file
         logger.debug(DBG_PROCESS, BIND10_CHECK_MSGQ_ALREADY_RUNNING)
-        # try to connect, and if we can't wait a short while
         try:
             self.cc_session = isc.cc.Session(self.msgq_socket_file)
             logger.fatal(BIND10_MSGQ_ALREADY_RUNNING)
@@ -735,10 +756,12 @@ class BoB:
             try:
                 (pid, exit_status) = self._get_process_exit_status()
             except OSError as o:
-                if o.errno == errno.ECHILD: break
+                if o.errno == errno.ECHILD:
+                    break
                 # XXX: should be impossible to get any other error here
                 raise
-            if pid == 0: break
+            if pid == 0:
+                break
             if pid in self.components:
                 # One of the components we know about.  Get information on it.
                 component = self.components.pop(pid)
@@ -909,8 +932,10 @@ class BoB:
         """
         if self._srv_socket is not None:
             self._srv_socket.close()
-            os.remove(self._socket_path)
-            os.rmdir(self._tmpdir)
+            if os.path.exists(self._socket_path):
+                os.remove(self._socket_path)
+            if os.path.isdir(self._tmpdir):
+                os.rmdir(self._tmpdir)
 
     def _srv_accept(self):
         """
diff --git a/src/bin/bind10/tests/bind10_test.py.in b/src/bin/bind10/tests/bind10_test.py.in
index ece6370..409790c 100644
--- a/src/bin/bind10/tests/bind10_test.py.in
+++ b/src/bin/bind10/tests/bind10_test.py.in
@@ -25,6 +25,7 @@ import bind10_src
 import unittest
 import sys
 import os
+import os.path
 import copy
 import signal
 import socket
@@ -34,6 +35,7 @@ import isc
 import isc.log
 import isc.bind10.socket_cache
 import errno
+import random
 
 from isc.testutils.parse_args import TestOptParser, OptsError
 from isc.testutils.ccsession_mock import MockModuleCCSession
@@ -366,6 +368,53 @@ class TestBoB(unittest.TestCase):
         self.assertEqual(creator, bob._socket_cache._creator)
         self.assertRaises(ValueError, bob.set_creator, creator)
 
+    def test_socket_srv(self):
+        """Tests init_socket_srv() and remove_socket_srv() work as expected."""
+        bob = BoB()
+
+        self.assertIsNone(bob._srv_socket)
+        self.assertIsNone(bob._tmpdir)
+        self.assertIsNone(bob._socket_path)
+
+        bob.init_socket_srv()
+
+        self.assertIsNotNone(bob._srv_socket)
+        self.assertNotEqual(-1, bob._srv_socket.fileno())
+        self.assertEqual(os.path.join(bob._tmpdir, 'sockcreator'),
+                         bob._srv_socket.getsockname())
+
+        self.assertIsNotNone(bob._tmpdir)
+        self.assertTrue(os.path.isdir(bob._tmpdir))
+        self.assertIsNotNone(bob._socket_path)
+        self.assertTrue(os.path.exists(bob._socket_path))
+
+        # Check that it's possible to connect to the socket file (this
+        # only works if the socket file exists and the server listens on
+        # it).
+        s = socket.socket(socket.AF_UNIX)
+        try:
+            s.connect(bob._socket_path)
+            can_connect = True
+            s.close()
+        except socket.error as e:
+            can_connect = False
+
+        self.assertTrue(can_connect)
+
+        bob.remove_socket_srv()
+
+        self.assertEqual(-1, bob._srv_socket.fileno())
+        self.assertFalse(os.path.exists(bob._socket_path))
+        self.assertFalse(os.path.isdir(bob._tmpdir))
+
+        # These should not fail either:
+
+        # second call
+        bob.remove_socket_srv()
+
+        bob._srv_socket = None
+        bob.remove_socket_srv()
+
     def test_init_alternate_socket(self):
         bob = BoB("alt_socket_file")
         self.assertEqual(bob.verbose, False)
@@ -461,6 +510,22 @@ class TestBoB(unittest.TestCase):
         self.assertEqual({'command': ['shutdown', {'pid': 42}]},
                          bob.cc_session.msg)
 
+# Mock class for testing BoB's usage of ProcessInfo
+class MockProcessInfo:
+    def __init__(self, name, args, env={}, dev_null_stdout=False,
+                 dev_null_stderr=False):
+        self.name = name
+        self.args = args
+        self.env = env
+        self.dev_null_stdout = dev_null_stdout
+        self.dev_null_stderr = dev_null_stderr
+        self.process = None
+        self.pid = None
+
+    def spawn(self):
+        # set some pid (only used for testing that it is not None anymore)
+        self.pid = 42147
+
 # Class for testing the BoB without actually starting processes.
 # This is used for testing the start/stop components routines and
 # the BoB commands.
@@ -490,6 +555,7 @@ class MockBob(BoB):
         self.c_channel_env = {}
         self.components = { }
         self.creator = False
+        self.get_process_exit_status_called = False
 
         class MockSockCreator(isc.bind10.component.Component):
             def __init__(self, process, boss, kind, address=None, params=None):
@@ -661,6 +727,52 @@ class MockBob(BoB):
             del self.components[12]
         self.cmdctl = False
 
+    def _get_process_exit_status(self):
+        if self.get_process_exit_status_called:
+            return (0, 0)
+        self.get_process_exit_status_called = True
+        return (53, 0)
+
+    def _get_process_exit_status_unknown_pid(self):
+        if self.get_process_exit_status_called:
+            return (0, 0)
+        self.get_process_exit_status_called = True
+        return (42, 0)
+
+    def _get_process_exit_status_raises_oserror_echild(self):
+        raise OSError(errno.ECHILD, 'Mock error')
+
+    def _get_process_exit_status_raises_oserror_other(self):
+        raise OSError(0, 'Mock error')
+
+    def _get_process_exit_status_raises_other(self):
+        raise Exception('Mock error')
+
+    def _make_mock_process_info(self, name, args, c_channel_env,
+                                dev_null_stdout=False, dev_null_stderr=False):
+        return MockProcessInfo(name, args, c_channel_env,
+                               dev_null_stdout, dev_null_stderr)
+
+class MockBobSimple(BoB):
+    def __init__(self):
+        BoB.__init__(self)
+        # Set which process has been started
+        self.started_process_name = None
+        self.started_process_args = None
+        self.started_process_env = None
+
+    def _make_mock_process_info(self, name, args, c_channel_env,
+                                dev_null_stdout=False, dev_null_stderr=False):
+        return MockProcessInfo(name, args, c_channel_env,
+                               dev_null_stdout, dev_null_stderr)
+
+    def start_process(self, name, args, c_channel_env, port=None,
+                      address=None):
+        self.started_process_name = name
+        self.started_process_args = args
+        self.started_process_env = c_channel_env
+        return None
+
 class TestStartStopProcessesBob(unittest.TestCase):
     """
     Check that the start_all_components method starts the right combination
@@ -930,6 +1042,9 @@ class MockComponent:
         self.pid = lambda: pid
         self.address = lambda: address
         self.restarted = False
+        self.forceful = False
+        self.running = True
+        self.has_failed = False
 
     def get_restart_time(self):
         return 0                # arbitrary dummy value
@@ -938,6 +1053,15 @@ class MockComponent:
         self.restarted = True
         return True
 
+    def is_running(self):
+        return self.running
+
+    def failed(self, status):
+        return self.has_failed
+
+    def kill(self, forceful):
+        self.forceful = forceful
+
 class TestBossCmd(unittest.TestCase):
     def test_ping(self):
         """
@@ -1107,6 +1231,20 @@ class TestBossComponents(unittest.TestCase):
                 'process': 'cat'
             }
         }
+        self._tmp_time = None
+        self._tmp_sleep = None
+        self._tmp_module_cc_session = None
+        self._tmp_cc_session = None
+
+    def tearDown(self):
+        if self._tmp_time is not None:
+            time.time = self._tmp_time
+        if self._tmp_sleep is not None:
+            time.sleep = self._tmp_sleep
+        if self._tmp_module_cc_session is not None:
+            isc.config.ModuleCCSession = self._tmp_module_cc_session
+        if self._tmp_cc_session is not None:
+            isc.cc.Session = self._tmp_cc_session
 
     def __unary_hook(self, param):
         """
@@ -1324,14 +1462,618 @@ class TestBossComponents(unittest.TestCase):
         bob._component_configurator._components['test'] = (None, component)
         self.__setup_restart(bob, component)
         self.assertTrue(component.restarted)
-        self.assertFalse(component in bob.components_to_restart)
+        self.assertNotIn(component, bob.components_to_restart)
 
         # Remove the component from the configuration.  It won't be restarted
         # even if scheduled, nor will remain in the to-be-restarted list.
         del bob._component_configurator._components['test']
         self.__setup_restart(bob, component)
         self.assertFalse(component.restarted)
-        self.assertFalse(component in bob.components_to_restart)
+        self.assertNotIn(component, bob.components_to_restart)
+
+    def test_get_processes(self):
+        '''Test that procsses are returned correctly, sorted by pid.'''
+        bob = MockBob()
+
+        pids = list(range(0, 20))
+        random.shuffle(pids)
+
+        for i in range(0, 20):
+            pid = pids[i]
+            component = MockComponent('test' + str(pid), pid,
+                                      'Test' + str(pid))
+            bob.components[pid] = component
+
+        process_list = bob.get_processes()
+        self.assertEqual(20, len(process_list))
+
+        last_pid = -1
+        for process in process_list:
+            pid = process[0]
+            self.assertLessEqual(last_pid, pid)
+            last_pid = pid
+            self.assertEqual([pid, 'test' + str(pid), 'Test' + str(pid)],
+                             process)
+
+    def _test_reap_children_helper(self, runnable, is_running, failed):
+        '''Construct a BoB instance, set various data in it according to
+        passed args and check if the component was added to the list of
+        components to restart.'''
+        bob = MockBob()
+        bob.runnable = runnable
+
+        component = MockComponent('test', 53)
+        component.running = is_running
+        component.has_failed = failed
+        bob.components[53] = component
+
+        self.assertNotIn(component, bob.components_to_restart)
+
+        bob.reap_children()
+
+        if runnable and is_running and not failed:
+            self.assertIn(component, bob.components_to_restart)
+        else:
+            self.assertEqual([], bob.components_to_restart)
+
+    def test_reap_children(self):
+        '''Test that children are queued to be restarted when they ask for it.'''
+        # test various combinations of 3 booleans
+        # (BoB.runnable, component.is_running(), component.failed())
+        self._test_reap_children_helper(False, False, False)
+        self._test_reap_children_helper(False, False, True)
+        self._test_reap_children_helper(False, True,  False)
+        self._test_reap_children_helper(False, True,  True)
+        self._test_reap_children_helper(True,  False, False)
+        self._test_reap_children_helper(True,  False, True)
+        self._test_reap_children_helper(True,  True,  False)
+        self._test_reap_children_helper(True,  True,  True)
+
+        # setup for more tests below
+        bob = MockBob()
+        bob.runnable = True
+        component = MockComponent('test', 53)
+        bob.components[53] = component
+
+        # case where the returned pid is unknown to us. nothing should
+        # happpen then.
+        bob.get_process_exit_status_called = False
+        bob._get_process_exit_status = bob._get_process_exit_status_unknown_pid
+        bob.components_to_restart = []
+        # this should do nothing as the pid is unknown
+        bob.reap_children()
+        self.assertEqual([], bob.components_to_restart)
+
+        # case where bob._get_process_exit_status() raises OSError with
+        # errno.ECHILD
+        bob._get_process_exit_status = \
+            bob._get_process_exit_status_raises_oserror_echild
+        bob.components_to_restart = []
+        # this should catch and handle the OSError
+        bob.reap_children()
+        self.assertEqual([], bob.components_to_restart)
+
+        # case where bob._get_process_exit_status() raises OSError with
+        # errno other than ECHILD
+        bob._get_process_exit_status = \
+            bob._get_process_exit_status_raises_oserror_other
+        with self.assertRaises(OSError):
+            bob.reap_children()
+
+        # case where bob._get_process_exit_status() raises something
+        # other than OSError
+        bob._get_process_exit_status = \
+            bob._get_process_exit_status_raises_other
+        with self.assertRaises(Exception):
+            bob.reap_children()
+
+    def test_kill_started_components(self):
+        '''Test that started components are killed.'''
+        bob = MockBob()
+
+        component = MockComponent('test', 53, 'Test')
+        bob.components[53] = component
+
+        self.assertEqual([[53, 'test', 'Test']], bob.get_processes())
+        bob.kill_started_components()
+        self.assertEqual([], bob.get_processes())
+        self.assertTrue(component.forceful)
+
+    def _start_msgq_helper(self, bob, verbose):
+        bob.verbose = verbose
+        pi = bob.start_msgq()
+        self.assertEqual('b10-msgq', pi.name)
+        self.assertEqual(['b10-msgq'], pi.args)
+        self.assertTrue(pi.dev_null_stdout)
+        self.assertEqual(pi.dev_null_stderr, not verbose)
+        self.assertEqual({'FOO': 'an env string'}, pi.env)
+
+        # this is set by ProcessInfo.spawn()
+        self.assertEqual(42147, pi.pid)
+
+    def test_start_msgq(self):
+        '''Test that b10-msgq is started.'''
+        bob = MockBobSimple()
+        bob.c_channel_env = {'FOO': 'an env string'}
+        bob._run_under_unittests = True
+
+        # use the MockProcessInfo creator
+        bob._make_process_info = bob._make_mock_process_info
+
+        # non-verbose case
+        self._start_msgq_helper(bob, False)
+
+        # verbose case
+        self._start_msgq_helper(bob, True)
+
+    def test_start_msgq_timeout(self):
+        '''Test that b10-msgq startup attempts connections several times
+        and times out eventually.'''
+        bob = MockBobSimple()
+        bob.c_channel_env = {}
+        # set the timeout to an arbitrary pre-determined value (which
+        # code below depends on)
+        bob.msgq_timeout = 1
+        bob._run_under_unittests = False
+
+        # use the MockProcessInfo creator
+        bob._make_process_info = bob._make_mock_process_info
+
+        global attempts
+        global tsec
+        attempts = 0
+        tsec = 0
+        self._tmp_time = time.time
+        self._tmp_sleep = time.sleep
+        def _my_time():
+            global attempts
+            global tsec
+            attempts += 1
+            return tsec
+        def _my_sleep(nsec):
+            global tsec
+            tsec += nsec
+        time.time = _my_time
+        time.sleep = _my_sleep
+
+        global cc_sub
+        cc_sub = None
+        class DummySessionAlwaysFails():
+            def __init__(self, socket_file):
+                raise isc.cc.session.SessionError('Connection fails')
+            def group_subscribe(self, s):
+                global cc_sub
+                cc_sub = s
+
+        isc.cc.Session = DummySessionAlwaysFails
+
+        with self.assertRaises(bind10_src.CChannelConnectError):
+            # An exception will be thrown here when it eventually times
+            # out.
+            pi = bob.start_msgq()
+
+        # time.time() should be called 12 times within the while loop:
+        # starting from 0, and 11 more times from 0.1 to 1.1. There's
+        # another call to time.time() outside the loop, which makes it
+        # 13.
+        self.assertEqual(attempts, 13)
+
+        # group_subscribe() should not have been called here.
+        self.assertIsNone(cc_sub)
+
+        global cc_socket_file
+        cc_socket_file = None
+        cc_sub = None
+        class DummySession():
+            def __init__(self, socket_file):
+                global cc_socket_file
+                cc_socket_file = socket_file
+            def group_subscribe(self, s):
+                global cc_sub
+                cc_sub = s
+
+        isc.cc.Session = DummySession
+
+        # reset values
+        attempts = 0
+        tsec = 0
+
+        pi = bob.start_msgq()
+
+        # just one attempt, but 2 calls to time.time()
+        self.assertEqual(attempts, 2)
+
+        self.assertEqual(cc_socket_file, bob.msgq_socket_file)
+        self.assertEqual(cc_sub, 'Boss')
+
+        # isc.cc.Session, time.time() and time.sleep() are restored
+        # during tearDown().
+
+    def _start_cfgmgr_helper(self, bob, data_path, filename, clear_config):
+        expect_args = ['b10-cfgmgr']
+        if data_path is not None:
+            bob.data_path = data_path
+            expect_args.append('--data-path=' + data_path)
+        if filename is not None:
+            bob.config_filename = filename
+            expect_args.append('--config-filename=' + filename)
+        if clear_config:
+            bob.clear_config = clear_config
+            expect_args.append('--clear-config')
+
+        pi = bob.start_cfgmgr()
+        self.assertEqual('b10-cfgmgr', pi.name)
+        self.assertEqual(expect_args, pi.args)
+        self.assertEqual({'TESTENV': 'A test string'}, pi.env)
+
+        # this is set by ProcessInfo.spawn()
+        self.assertEqual(42147, pi.pid)
+
+    def test_start_cfgmgr(self):
+        '''Test that b10-cfgmgr is started.'''
+        class DummySession():
+            def __init__(self):
+                self._tries = 0
+            def group_recvmsg(self):
+                self._tries += 1
+                # return running on the 3rd try onwards
+                if self._tries >= 3:
+                    return ({'running': 'ConfigManager'}, None)
+                else:
+                    return ({}, None)
+
+        bob = MockBobSimple()
+        bob.c_channel_env = {'TESTENV': 'A test string'}
+        bob.cc_session = DummySession()
+        bob.wait_time = 5
+
+        # use the MockProcessInfo creator
+        bob._make_process_info = bob._make_mock_process_info
+
+        global attempts
+        attempts = 0
+        self._tmp_sleep = time.sleep
+        def _my_sleep(nsec):
+            global attempts
+            attempts += 1
+        time.sleep = _my_sleep
+
+        # defaults
+        self._start_cfgmgr_helper(bob, None, None, False)
+
+        # check that 2 attempts were made. on the 3rd attempt,
+        # process_running() returns that ConfigManager is running.
+        self.assertEqual(attempts, 2)
+
+        # data_path is specified
+        self._start_cfgmgr_helper(bob, '/var/lib/test', None, False)
+
+        # config_filename is specified. Because `bob` is not
+        # reconstructed, data_path is retained from the last call to
+        # _start_cfgmgr_helper().
+        self._start_cfgmgr_helper(bob, '/var/lib/test', 'foo.cfg', False)
+
+        # clear_config is specified. Because `bob` is not reconstructed,
+        # data_path and config_filename are retained from the last call
+        # to _start_cfgmgr_helper().
+        self._start_cfgmgr_helper(bob, '/var/lib/test', 'foo.cfg', True)
+
+    def test_start_cfgmgr_timeout(self):
+        '''Test that b10-cfgmgr startup attempts connections several times
+        and times out eventually.'''
+        class DummySession():
+            def group_recvmsg(self):
+                return (None, None)
+        bob = MockBobSimple()
+        bob.c_channel_env = {}
+        bob.cc_session = DummySession()
+        # set wait_time to an arbitrary pre-determined value (which code
+        # below depends on)
+        bob.wait_time = 2
+
+        # use the MockProcessInfo creator
+        bob._make_process_info = bob._make_mock_process_info
+
+        global attempts
+        attempts = 0
+        self._tmp_sleep = time.sleep
+        def _my_sleep(nsec):
+            global attempts
+            attempts += 1
+        time.sleep = _my_sleep
+
+        # We just check that an exception was thrown, and that several
+        # attempts were made to connect.
+        with self.assertRaises(bind10_src.ProcessStartError):
+            pi = bob.start_cfgmgr()
+
+        # 2 seconds of attempts every 1 second should result in 2 attempts
+        self.assertEqual(attempts, 2)
+
+        # time.sleep() is restored during tearDown().
+
+    def test_start_ccsession(self):
+        '''Test that CC session is started.'''
+        class DummySession():
+            def __init__(self, specfile, config_handler, command_handler,
+                         socket_file):
+                self.specfile = specfile
+                self.config_handler = config_handler
+                self.command_handler = command_handler
+                self.socket_file = socket_file
+                self.started = False
+            def start(self):
+                self.started = True
+        bob = MockBobSimple()
+        self._tmp_module_cc_session = isc.config.ModuleCCSession
+        isc.config.ModuleCCSession = DummySession
+
+        bob.start_ccsession({})
+        self.assertEqual(bind10_src.SPECFILE_LOCATION, bob.ccs.specfile)
+        self.assertEqual(bob.config_handler, bob.ccs.config_handler)
+        self.assertEqual(bob.command_handler, bob.ccs.command_handler)
+        self.assertEqual(bob.msgq_socket_file, bob.ccs.socket_file)
+        self.assertTrue(bob.ccs.started)
+
+        # isc.config.ModuleCCSession is restored during tearDown().
+
+    def test_start_process(self):
+        '''Test that processes can be started.'''
+        bob = MockBob()
+
+        # use the MockProcessInfo creator
+        bob._make_process_info = bob._make_mock_process_info
+
+        pi = bob.start_process('Test Process', ['/bin/true'], {})
+        self.assertEqual('Test Process', pi.name)
+        self.assertEqual(['/bin/true'], pi.args)
+        self.assertEqual({}, pi.env)
+
+        # this is set by ProcessInfo.spawn()
+        self.assertEqual(42147, pi.pid)
+
+    def test_register_process(self):
+        '''Test that processes can be registered with BoB.'''
+        bob = MockBob()
+        component = MockComponent('test', 53, 'Test')
+
+        self.assertFalse(53 in bob.components)
+        bob.register_process(53, component)
+        self.assertTrue(53 in bob.components)
+        self.assertEqual(bob.components[53].name(), 'test')
+        self.assertEqual(bob.components[53].pid(), 53)
+        self.assertEqual(bob.components[53].address(), 'Test')
+
+    def _start_simple_helper(self, bob, verbose):
+        bob.verbose = verbose
+
+        args = ['/bin/true']
+        if verbose:
+            args.append('-v')
+
+        bob.start_simple('/bin/true')
+        self.assertEqual('/bin/true', bob.started_process_name)
+        self.assertEqual(args, bob.started_process_args)
+        self.assertEqual({'TESTENV': 'A test string'}, bob.started_process_env)
+
+    def test_start_simple(self):
+        '''Test simple process startup.'''
+        bob = MockBobSimple()
+        bob.c_channel_env = {'TESTENV': 'A test string'}
+
+        # non-verbose case
+        self._start_simple_helper(bob, False)
+
+        # verbose case
+        self._start_simple_helper(bob, True)
+
+    def _start_auth_helper(self, bob, verbose):
+        bob.verbose = verbose
+
+        args = ['b10-auth']
+        if verbose:
+            args.append('-v')
+
+        bob.start_auth()
+        self.assertEqual('b10-auth', bob.started_process_name)
+        self.assertEqual(args, bob.started_process_args)
+        self.assertEqual({'FOO': 'an env string'}, bob.started_process_env)
+
+    def test_start_auth(self):
+        '''Test that b10-auth is started.'''
+        bob = MockBobSimple()
+        bob.c_channel_env = {'FOO': 'an env string'}
+
+        # non-verbose case
+        self._start_auth_helper(bob, False)
+
+        # verbose case
+        self._start_auth_helper(bob, True)
+
+    def _start_resolver_helper(self, bob, verbose):
+        bob.verbose = verbose
+
+        args = ['b10-resolver']
+        if verbose:
+            args.append('-v')
+
+        bob.start_resolver()
+        self.assertEqual('b10-resolver', bob.started_process_name)
+        self.assertEqual(args, bob.started_process_args)
+        self.assertEqual({'BAR': 'an env string'}, bob.started_process_env)
+
+    def test_start_resolver(self):
+        '''Test that b10-resolver is started.'''
+        bob = MockBobSimple()
+        bob.c_channel_env = {'BAR': 'an env string'}
+
+        # non-verbose case
+        self._start_resolver_helper(bob, False)
+
+        # verbose case
+        self._start_resolver_helper(bob, True)
+
+    def _start_cmdctl_helper(self, bob, verbose, port = None):
+        bob.verbose = verbose
+
+        args = ['b10-cmdctl']
+
+        if port is not None:
+            bob.cmdctl_port = port
+            args.append('--port=9353')
+
+        if verbose:
+            args.append('-v')
+
+        bob.start_cmdctl()
+        self.assertEqual('b10-cmdctl', bob.started_process_name)
+        self.assertEqual(args, bob.started_process_args)
+        self.assertEqual({'BAZ': 'an env string'}, bob.started_process_env)
+
+    def test_start_cmdctl(self):
+        '''Test that b10-cmdctl is started.'''
+        bob = MockBobSimple()
+        bob.c_channel_env = {'BAZ': 'an env string'}
+
+        # non-verbose case
+        self._start_cmdctl_helper(bob, False)
+
+        # verbose case
+        self._start_cmdctl_helper(bob, True)
+
+        # with port, non-verbose case
+        self._start_cmdctl_helper(bob, False, 9353)
+
+        # with port, verbose case
+        self._start_cmdctl_helper(bob, True, 9353)
+
+    def test_socket_data(self):
+        '''Test that BoB._socket_data works as expected.'''
+        class MockSock:
+            def __init__(self, fd, throw):
+                self.fd = fd
+                self.throw = throw
+                self.buf = b'Hello World.\nYou are so nice today.\nXX'
+                self.i = 0
+
+            def recv(self, bufsize, flags = 0):
+                if bufsize != 1:
+                    raise Exception('bufsize != 1')
+                if flags != socket.MSG_DONTWAIT:
+                    raise Exception('flags != socket.MSG_DONTWAIT')
+                # after 15 recv()s, throw a socket.error with EAGAIN to
+                # get _socket_data() to save back what's been read. The
+                # number 15 is arbitrarily chosen, but the checks then
+                # depend on this being 15, i.e., if you adjust this
+                # number, you may have to adjust the checks below too.
+                if self.throw and self.i > 15:
+                    raise socket.error(errno.EAGAIN, 'Try again')
+                if self.i >= len(self.buf):
+                    return b'';
+                t = self.i
+                self.i += 1
+                return self.buf[t:t+1]
+
+            def close(self):
+                return
+
+        class MockBobSocketData(BoB):
+            def __init__(self, throw):
+                self._unix_sockets = {42: (MockSock(42, throw), b'')}
+                self.requests = []
+                self.dead = []
+
+            def socket_request_handler(self, previous, sock):
+                self.requests.append({sock.fd: previous})
+
+            def socket_consumer_dead(self, sock):
+                self.dead.append(sock.fd)
+
+        # Case where we get data every time we call recv()
+        bob = MockBobSocketData(False)
+        bob._socket_data(42)
+        self.assertEqual(bob.requests,
+                         [{42: b'Hello World.'},
+                          {42: b'You are so nice today.'}])
+        self.assertEqual(bob.dead, [42])
+        self.assertEqual({}, bob._unix_sockets)
+
+        # Case where socket.recv() raises EAGAIN. In this case, the
+        # routine is supposed to save what it has back to
+        # BoB._unix_sockets.
+        bob = MockBobSocketData(True)
+        bob._socket_data(42)
+        self.assertEqual(bob.requests, [{42: b'Hello World.'}])
+        self.assertFalse(bob.dead)
+        self.assertEqual(len(bob._unix_sockets), 1)
+        self.assertEqual(bob._unix_sockets[42][1], b'You')
+
+    def test_startup(self):
+        '''Test that BoB.startup() handles failures properly.'''
+        class MockBobStartup(BoB):
+            def __init__(self, throw):
+                self.throw = throw
+                self.started = False
+                self.killed = False
+                self.msgq_socket_file = None
+                self.curproc = 'myproc'
+                self.runnable = False
+
+            def start_all_components(self):
+                self.started = True
+                if self.throw:
+                    raise Exception('Assume starting components has failed.')
+
+            def kill_started_components(self):
+                self.killed = True
+
+        class DummySession():
+            def __init__(self, socket_file):
+                raise isc.cc.session.SessionError('This is the expected case.')
+
+        class DummySessionSocketExists():
+            def __init__(self, socket_file):
+                # simulate that connect passes
+                return
+
+        isc.cc.Session = DummySession
+
+        # All is well case, where all components are started
+        # successfully. We check that the actual call to
+        # start_all_components() is made, and BoB.runnable is true.
+        bob = MockBobStartup(False)
+        r = bob.startup()
+        self.assertIsNone(r)
+        self.assertTrue(bob.started)
+        self.assertFalse(bob.killed)
+        self.assertTrue(bob.runnable)
+        self.assertEqual({}, bob.c_channel_env)
+
+        # Case where starting components fails. We check that
+        # kill_started_components() is called right after, and
+        # BoB.runnable is not modified.
+        bob = MockBobStartup(True)
+        r = bob.startup()
+        # r contains an error message
+        self.assertEqual(r, 'Unable to start myproc: Assume starting components has failed.')
+        self.assertTrue(bob.started)
+        self.assertTrue(bob.killed)
+        self.assertFalse(bob.runnable)
+        self.assertEqual({}, bob.c_channel_env)
+
+        # Check if msgq_socket_file is carried over
+        bob = MockBobStartup(False)
+        bob.msgq_socket_file = 'foo'
+        r = bob.startup()
+        self.assertEqual({'BIND10_MSGQ_SOCKET_FILE': 'foo'}, bob.c_channel_env)
+
+        # Check the case when socket file already exists
+        isc.cc.Session = DummySessionSocketExists
+        bob = MockBobStartup(False)
+        r = bob.startup()
+        self.assertIn('already running', r)
+
+        # isc.cc.Session is restored during tearDown().
 
 class SocketSrvTest(unittest.TestCase):
     """
@@ -1563,6 +2305,25 @@ class TestFunctions(unittest.TestCase):
         # second call should not assert anyway
         bind10_src.remove_lock_files()
 
+    def test_get_signame(self):
+        # just test with some samples
+        signame = bind10_src.get_signame(signal.SIGTERM)
+        self.assertEqual('SIGTERM', signame)
+        signame = bind10_src.get_signame(signal.SIGKILL)
+        self.assertEqual('SIGKILL', signame)
+        # 59426 is hopefully an unused signal on most platforms
+        signame = bind10_src.get_signame(59426)
+        self.assertEqual('Unknown signal 59426', signame)
+
+    def test_fatal_signal(self):
+        self.assertIsNone(bind10_src.boss_of_bind)
+        bind10_src.boss_of_bind = BoB()
+        bind10_src.boss_of_bind.runnable = True
+        bind10_src.fatal_signal(signal.SIGTERM, None)
+        # Now, runnable must be False
+        self.assertFalse(bind10_src.boss_of_bind.runnable)
+        bind10_src.boss_of_bind = None
+
 if __name__ == '__main__':
     # store os.environ for test_unchanged_environment
     original_os_environ = copy.deepcopy(os.environ)



More information about the bind10-changes mailing list