Browse Source

[213] Parts of the configurator startup

The tests are complete, the implementation of _build_plan is still
missing.
Michal 'vorner' Vaner 13 years ago
parent
commit
be3038ae1b

+ 55 - 2
src/lib/python/isc/bind10/component.py

@@ -163,6 +163,11 @@ class Configurator:
     This thing keeps track of configuration changes and starts and stops
     This thing keeps track of configuration changes and starts and stops
     components as it goes. It also handles the inital startup and final
     components as it goes. It also handles the inital startup and final
     shutdown.
     shutdown.
+
+    Note that this will allow you to stop (by invoking reconfigure) a core
+    component. There should be some kind of layer protecting users from ever
+    doing so (users must not stop the config manager, message queue and stuff
+    like that or the system won't start again).
     """
     """
     def __init__(self, boss):
     def __init__(self, boss):
         """
         """
@@ -170,7 +175,12 @@ class Configurator:
 
 
         The boss parameter is the boss object used to start and stop processes.
         The boss parameter is the boss object used to start and stop processes.
         """
         """
-        pass
+        self.__boss = boss
+        # These could be __private, but as we access them from within unittest,
+        # it's more comfortable to have them just _protected.
+        self._components = {}
+        self._old_config = {}
+        self._running = False
 
 
     def startup(self, configuration):
     def startup(self, configuration):
         """
         """
@@ -178,7 +188,12 @@ class Configurator:
         to be hardcoded from the boss itself to start the configuration
         to be hardcoded from the boss itself to start the configuration
         manager and other similar things.
         manager and other similar things.
         """
         """
-        pass
+        if self._running:
+            raise ValueError("Trying to start the component configurator " +
+                             "twice")
+        self._run_plan(self._build_plan({}, configuration))
+        self._old_config = configuration
+        self._running = True
 
 
     def shutdown(self):
     def shutdown(self):
         """
         """
@@ -192,3 +207,41 @@ class Configurator:
         starts and stops all the components as needed.
         starts and stops all the components as needed.
         """
         """
         pass
         pass
+
+    def _build_plan(self, old, new):
+        """
+        Builds a plan how to transfer from the old configuration to the new
+        one. It'll be sorted by priority and it will contain the components
+        (already created, but not started). Each command in the plan is a dict,
+        so it can be extended any time in future to include whatever
+        parameters each operation might need.
+
+        Any configuration problems are expected to be handled here, so the
+        plan is not yet run.
+        """
+        pass
+
+    def _run_plan(self, plan):
+        """
+        Run a plan, created beforehead by _build_plan.
+
+        With the start and stop commands, it also adds and removes components
+        in _components.
+
+        Currently implemented commands are:
+        * start
+        * stop
+        """
+        for task in plan:
+            component = task['component']
+            command = task['command']
+            if command == 'start':
+                component.start()
+                self._components[task['name']] = component
+            elif command == 'stop':
+                component.stop()
+                del self._components[task['name']]
+            else:
+                # Can Not Happen (as the plans are generated by ourself).
+                # Therefore not tested.
+                raise NotImplementedError("Command unknown: " + command)

+ 171 - 26
src/lib/python/isc/bind10/tests/component_test.py

@@ -20,6 +20,7 @@ Tests for the bind10.component module
 import unittest
 import unittest
 import isc.log
 import isc.log
 import time
 import time
+import copy
 from isc.bind10.component import Component, Configurator, specials
 from isc.bind10.component import Component, Configurator, specials
 
 
 class TestError(Exception):
 class TestError(Exception):
@@ -28,25 +29,24 @@ class TestError(Exception):
     """
     """
     pass
     pass
 
 
-class ComponentTests(unittest.TestCase):
+class BossUtils:
     """
     """
-    Tests for the bind10.component.Component class
+    A class that brings some utilities for pretending we're Boss.
+    This is expected to be inherited by the testcases themself.
     """
     """
     def setUp(self):
     def setUp(self):
         """
         """
-        Pretend a newly started system.
+        Part of setup. Should be called by descendand's setUp.
         """
         """
-        self.__shutdown = False
-        self.__exitcode = None
-        self.__start_called = False
-        self.__stop_called = False
-        self.__failed_called = False
+        self._shutdown = False
+        self._exitcode = None
         # Back up the time function, we may want to replace it with something
         # Back up the time function, we may want to replace it with something
         self.__orig_time = isc.bind10.component.time.time
         self.__orig_time = isc.bind10.component.time.time
 
 
     def tearDown(self):
     def tearDown(self):
         """
         """
-        Clean up after tests.
+        Clean up after tests. If the descendand implements a tearDown, it
+        should call this method internally.
         """
         """
         # Return the original time function
         # Return the original time function
         isc.bind10.component.time.time = self.__orig_time
         isc.bind10.component.time.time = self.__orig_time
@@ -55,10 +55,10 @@ class ComponentTests(unittest.TestCase):
         """
         """
         Mock function to shut down. We just note we were asked to do so.
         Mock function to shut down. We just note we were asked to do so.
         """
         """
-        self.__shutdown = True
-        self.__exitcode = None
+        self._shutdown = True
+        self._exitcode = None
 
 
-    def __timeskip(self):
+    def _timeskip(self):
         """
         """
         Skip in time to future some 30s. Implemented by replacing the
         Skip in time to future some 30s. Implemented by replacing the
         time.time function in the tested module with function that returns
         time.time function in the tested module with function that returns
@@ -67,6 +67,21 @@ class ComponentTests(unittest.TestCase):
         tm = time.time()
         tm = time.time()
         isc.bind10.component.time.time = lambda: tm + 30
         isc.bind10.component.time.time = lambda: tm + 30
 
 
+class ComponentTests(BossUtils, unittest.TestCase):
+    """
+    Tests for the bind10.component.Component class
+    """
+    def setUp(self):
+        """
+        Pretend a newly started system.
+        """
+        BossUtils.setUp(self)
+        self._shutdown = False
+        self._exitcode = None
+        self.__start_called = False
+        self.__stop_called = False
+        self.__failed_called = False
+
     def __start(self):
     def __start(self):
         """
         """
         Mock function, installed into the component into start_internal.
         Mock function, installed into the component into start_internal.
@@ -120,7 +135,7 @@ class ComponentTests(unittest.TestCase):
         Check that nothing was called yet. A newly created component should
         Check that nothing was called yet. A newly created component should
         not get started right away, so this should pass after the creation.
         not get started right away, so this should pass after the creation.
         """
         """
-        self.assertFalse(self.__shutdown)
+        self.assertFalse(self._shutdown)
         self.assertFalse(self.__start_called)
         self.assertFalse(self.__start_called)
         self.assertFalse(self.__stop_called)
         self.assertFalse(self.__stop_called)
         self.assertFalse(self.__failed_called)
         self.assertFalse(self.__failed_called)
@@ -133,7 +148,7 @@ class ComponentTests(unittest.TestCase):
         """
         """
         Check the component was started, but not stopped anyhow yet.
         Check the component was started, but not stopped anyhow yet.
         """
         """
-        self.assertFalse(self.__shutdown)
+        self.assertFalse(self._shutdown)
         self.assertTrue(self.__start_called)
         self.assertTrue(self.__start_called)
         self.assertFalse(self.__stop_called)
         self.assertFalse(self.__stop_called)
         self.assertFalse(self.__failed_called)
         self.assertFalse(self.__failed_called)
@@ -143,11 +158,11 @@ class ComponentTests(unittest.TestCase):
         """
         """
         Check the component is completely dead, and the server too.
         Check the component is completely dead, and the server too.
         """
         """
-        self.assertTrue(self.__shutdown)
+        self.assertTrue(self._shutdown)
         self.assertTrue(self.__start_called)
         self.assertTrue(self.__start_called)
         self.assertFalse(self.__stop_called)
         self.assertFalse(self.__stop_called)
         self.assertTrue(self.__failed_called)
         self.assertTrue(self.__failed_called)
-        self.assertNotEqual(0, self.__exitcode)
+        self.assertNotEqual(0, self._exitcode)
         self.assertFalse(component.running())
         self.assertFalse(component.running())
         # Surely it can't be stopped again
         # Surely it can't be stopped again
         self.assertRaises(ValueError, component.stop)
         self.assertRaises(ValueError, component.stop)
@@ -166,7 +181,7 @@ class ComponentTests(unittest.TestCase):
         Reset the self.__start_called to False before calling the function when
         Reset the self.__start_called to False before calling the function when
         the component should fail.
         the component should fail.
         """
         """
-        self.assertFalse(self.__shutdown)
+        self.assertFalse(self._shutdown)
         self.assertTrue(self.__start_called)
         self.assertTrue(self.__start_called)
         self.assertFalse(self.__stop_called)
         self.assertFalse(self.__stop_called)
         self.assertTrue(self.__failed_called)
         self.assertTrue(self.__failed_called)
@@ -192,7 +207,7 @@ class ComponentTests(unittest.TestCase):
         self.assertRaises(ValueError, component.start)
         self.assertRaises(ValueError, component.start)
         # Stop it again and check
         # Stop it again and check
         component.stop()
         component.stop()
-        self.assertFalse(self.__shutdown)
+        self.assertFalse(self._shutdown)
         self.assertTrue(self.__start_called)
         self.assertTrue(self.__start_called)
         self.assertTrue(self.__stop_called)
         self.assertTrue(self.__stop_called)
         self.assertFalse(self.__failed_called)
         self.assertFalse(self.__failed_called)
@@ -245,7 +260,7 @@ class ComponentTests(unittest.TestCase):
         self.__check_startup(component)
         self.__check_startup(component)
         component.start()
         component.start()
         self.__check_started(component)
         self.__check_started(component)
-        self.__timeskip()
+        self._timeskip()
         # Pretend the componend died some time later
         # Pretend the componend died some time later
         component.failed()
         component.failed()
         # Check the component is still dead
         # Check the component is still dead
@@ -278,7 +293,7 @@ class ComponentTests(unittest.TestCase):
         self.__check_started(component)
         self.__check_started(component)
         # Make it fail later on
         # Make it fail later on
         self.__start_called = False
         self.__start_called = False
-        self.__timeskip()
+        self._timeskip()
         component.failed()
         component.failed()
         self.__check_restarted(component)
         self.__check_restarted(component)
 
 
@@ -308,7 +323,7 @@ class ComponentTests(unittest.TestCase):
         self.__check_started(component)
         self.__check_started(component)
         # Make it fail later on
         # Make it fail later on
         self.__start_called = False
         self.__start_called = False
-        self.__timeskip()
+        self._timeskip()
         component.failed()
         component.failed()
         self.__check_restarted(component)
         self.__check_restarted(component)
 
 
@@ -369,6 +384,7 @@ class TestComponent(Component):
         self.__owner = owner
         self.__owner = owner
         self.__name = name
         self.__name = name
         self.log('init')
         self.log('init')
+        self.log(kind)
 
 
     def log(self, event):
     def log(self, event):
         """
         """
@@ -386,25 +402,66 @@ class TestComponent(Component):
     def failed_internal(self):
     def failed_internal(self):
         self.log('failed')
         self.log('failed')
 
 
-class ConfiguratorTest(unittest.TestCase):
+class FailComponent(Component):
+    """
+    A mock component that fails whenever it is started.
+    """
+    def start_internal(self):
+        raise TestError("test error")
+
+class ConfiguratorTest(BossUtils, unittest.TestCase):
     """
     """
     Tests for the configurator.
     Tests for the configurator.
     """
     """
     def setUp(self):
     def setUp(self):
         """
         """
         Insert the special evaluated test components we use and prepare the
         Insert the special evaluated test components we use and prepare the
-        log.
+        log. Also provide some data for the tests and prepare us to pretend
+        we're boss.
         """
         """
+        BossUtils.setUp(self)
         # We put our functions inside instead of class constructors,
         # We put our functions inside instead of class constructors,
         # so we can look into what is happening more easily
         # so we can look into what is happening more easily
-        self.__orig_specials = copy(specials)
+        self.__orig_specials = copy.copy(specials)
         specials['test'] = self.__component_test
         specials['test'] = self.__component_test
-        log = []
+        self.log = []
+        # The core "hardcoded" configuration
+        self.__core = {
+            'core1': {
+                'priority': 5,
+                'process': 'core1',
+                'special': 'test',
+                'kind': 'core'
+            },
+            'core2': {
+                'process': 'core2',
+                'special': 'test',
+                'kind': 'core'
+            },
+            'core3': {
+                'process': 'core3',
+                'priority': 3,
+                'special': 'test',
+                'kind': 'core'
+            }
+        }
+        # How they should be started. The are created in the order they are
+        # found in the dict, but then they should be started by priority.
+        # This expects that the same dict returns its keys in the same order
+        # every time
+        self.__core_log_create = []
+        for core in self.__core.keys():
+            self.__core_log_create.append((core, 'init'))
+            self.__core_log_create.append((core, 'core'))
+        self.__core_log_start = [('core1', 'start'), ('core3', 'start'),
+                                 ('core2', 'start')]
+        self.__core_log = self.__core_log_create + self.__core_log_start
 
 
     def tearDown(self):
     def tearDown(self):
         """
         """
-        Clean up the special evaluated test components.
+        Clean up the special evaluated test components and other stuff.
         """
         """
+        BossUtils.tearDown(self)
         specials = self.__orig_specials
         specials = self.__orig_specials
 
 
     def __component_test(self, process, boss, kind):
     def __component_test(self, process, boss, kind):
@@ -414,6 +471,94 @@ class ConfiguratorTest(unittest.TestCase):
         self.assertEqual(self, boss)
         self.assertEqual(self, boss)
         return TestComponent(self, process, kind)
         return TestComponent(self, process, kind)
 
 
+    def test_init(self):
+        """
+        Tests the configurator can be created and it does not create
+        any components yet, nor does it remember anything.
+        """
+        configurator = Configurator(self)
+        self.assertEqual([], self.log)
+        self.assertEqual({}, configurator._components)
+        self.assertEqual({}, configurator._old_config)
+        self.assertFalse(configurator._running)
+
+    def test_run_plan(self):
+        """
+        Test the internal function of running plans. Just see it can handle
+        the commands in the given order. We see that by the log.
+
+        Also includes one that raises, so we see it just stops there.
+        """
+        # Prepare the configurator and the plan
+        configurator = Configurator(self)
+        started = self.__component_test('second', self, 'dispensable')
+        started.start()
+        stopped = self.__component_test('first', self, 'core')
+        configurator._components = {'second': started}
+        plan = [
+            {
+                'component': stopped,
+                'command': 'start',
+                'name': 'first'
+            },
+            {
+                'component': started,
+                'command': 'stop',
+                'name': 'second'
+            },
+            {
+                'component': FailComponent('third', self, 'needed'),
+                'command': 'start',
+                'name': 'third'
+            },
+            {
+                'component': self.__component_test('fourth', self, 'core'),
+                'command': 'start',
+                'name': 'fourth'
+            }
+        ]
+        # Don't include the preparation into the log
+        self.log = []
+        # The error from the third component is propagated
+        self.assertRaises(TestError, configurator._run_plan, plan)
+        # The first two were handled, the rest not, due to the exception
+        self.assertEqual([('first', 'start'), ('second', 'stop')], self.log)
+        self.assertEqual({'first': stopped}, configurator._components)
+
+    def test_build_plan(self):
+        """
+        Test building the plan correctly. Currently, we let it only create
+        the initial plan when the old configuration is empty, as we don't need
+        more for the starts.
+        """
+        configurator = Configurator(self)
+        plan = configurator._build_plan({}, self.__core)
+        # This should have created the components
+        self.assertEqual(self.__core_log_create, self.log)
+        self.assertEqual(3, len(plan))
+        for (task, name) in zip(plan, ['core1', 'core3', 'core2']):
+            self.assertTrue('component' in task)
+            self.assertEqual('start', task['command'])
+            self.assertEqual(name, task['name'])
+        # TODO: More scenarios for changes between configurations are needed
+
+    def test_startup(self):
+        """
+        Passes some configuration to the startup method and sees if
+        the components are started up.
+
+        It also checks the components are kept inside the configurator.
+        """
+        configurator = Configurator(self)
+        configurator.startup(self.__core)
+        self.assertEqual(self.__core_log, self.log)
+        for core in self.__core.keys():
+            self.assertTrue(core in configurator._components)
+        self.assertEqual(self.__core, configurator._old_config)
+        self.assertTrue(configurator._running)
+        # It can't be started twice
+        self.assertRaises(ValueError, configurator.startup, self.__core)
+
 if __name__ == '__main__':
 if __name__ == '__main__':
     isc.log.init("bind10") # FIXME Should this be needed?
     isc.log.init("bind10") # FIXME Should this be needed?
     isc.log.resetUnitTestRootLogger()
     isc.log.resetUnitTestRootLogger()