Browse Source

[213-incremental] Merge branch 'trac213' into trac213-incremental, reverting
bind10 related changes.

JINMEI Tatuya 13 years ago
parent
commit
dd63399d28

+ 0 - 6
src/bin/bind10/TODO

@@ -1,19 +1,13 @@
 - Read msgq configuration from configuration manager (Trac #213)
   https://bind10.isc.org/ticket/213
 - Provide more administrator options:
-  - Get process list
   - Get information on a process (returns list of times started & stopped, 
     plus current information such as PID)
-  - Add a component (not necessary for parking lot, but...)
   - Stop a component
   - Force-stop a component
 - Mechanism to wait for child to start before continuing
-- Way to ask a child to die politely 
-- Start statistics daemon
-- Statistics interaction (?)
 - Use .spec file to define comands
 - Rename "c-channel" stuff to msgq for clarity
-- Use logger
 - Reply to shutdown message?
 - Some sort of group creation so termination signals can be sent to
   children of children processes (if any)

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

@@ -1,15 +1,8 @@
 SUBDIRS = isc
 
-python_PYTHON =	bind10_config.py
+nodist_python_PYTHON =	bind10_config.py
 pythondir = $(pyexecdir)
 
-# Explicitly define DIST_COMMON so ${python_PYTHON} is not included
-# as we don't want the generated file included in distributed tarfile.
-DIST_COMMON = $(srcdir)/Makefile.am $(srcdir)/Makefile.in bind10_config.py.in
-
-# When setting DIST_COMMON, then need to add the .in file too.
-EXTRA_DIST =  bind10_config.py.in
-
 CLEANFILES = bind10_config.pyc
 CLEANDIRS = __pycache__
 

+ 4 - 0
src/lib/python/bind10_config.py.in

@@ -23,6 +23,10 @@ def reload():
     global DATA_PATH
     global PLUGIN_PATHS
     global PREFIX
+    global LIBEXECDIR
+    LIBEXECDIR = ("@libexecdir@/@PACKAGE@"). \
+        replace("${exec_prefix}", "@exec_prefix@"). \
+        replace("${prefix}", "@prefix@")
     BIND10_MSGQ_SOCKET_FILE = os.path.join("@localstatedir@",
                                            "@PACKAGE_NAME@",
                                            "msgq_socket").replace("${prefix}",

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

@@ -1,4 +1,4 @@
 SUBDIRS = . tests
 
-python_PYTHON = __init__.py sockcreator.py
+python_PYTHON = __init__.py sockcreator.py component.py special_component.py
 pythondir = $(pyexecdir)/isc/bind10

+ 503 - 0
src/lib/python/isc/bind10/component.py

@@ -0,0 +1,503 @@
+# Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""
+Module for managing components (abstraction of process). It allows starting
+them in given order, handling when they crash (what happens depends on kind
+of component) and shutting down. It also handles the configuration of this.
+
+Dependencies between them are not yet handled. It might turn out they are
+needed, in that case they will be added sometime in future.
+"""
+
+import isc.log
+from isc.log_messages.bind10_messages import *
+import time
+
+logger = isc.log.Logger("boss")
+DBG_TRACE_DATA = 20
+DBG_TRACE_DETAILED = 80
+
+START_CMD = 'start'
+STOP_CMD = 'stop'
+
+STARTED_OK_TIME = 10
+
+STATE_DEAD = 'dead'
+STATE_STOPPED = 'stopped'
+STATE_RUNNING = 'running'
+
+class Component:
+    """
+    This represents a single component. It has some defaults of behaviour,
+    which should be reasonable for majority of ordinary components, but
+    it might be inherited and modified for special-purpose components,
+    like the core modules with different ways of starting up. Another
+    way to tweak only the start of the component (eg. by providing some
+    command line parameters) is to set _start_func function from within
+    inherited class.
+
+    The methods are marked if it is expected for them to be overridden.
+
+
+    The component is in one of the three states:
+    - Stopped - it is either not started yet or it was explicitly stopped.
+      The component is created in this state (it must be asked to start
+      explicitly).
+    - Running - after start() was called, it started successfully and is
+      now running.
+    - Dead - it failed and can not be resurrected.
+
+    Init
+      |            stop()
+      |  +-----------------------+
+      |  |                       |
+      v  |  start()  success     |
+    Stopped --------+--------> Running <----------+
+                    |            |                |
+                    |failure     | failed()       |
+                    |            |                |
+                    v            |                |
+                    +<-----------+                |
+                    |                             |
+                    |  kind == dispensable or kind|== needed and failed late
+                    +-----------------------------+
+                    |
+                    | kind == core or kind == needed and it failed too soon
+                    v
+                  Dead
+    """
+    def __init__(self, process, boss, kind, address=None, params=None):
+        """
+        Creates the component in not running mode.
+
+        The parameters are:
+        - `process` is the name of the process to start.
+        - `boss` the boss object to plug into. The component needs to plug
+          into it to know when it failed, etc.
+        - `kind` is the kind of component. It may be one of:
+          * 'core' means the system can't run without it and it can't be
+            safely restarted. If it does not start, the system is brought
+            down. If it crashes, the system is turned off as well (with
+            non-zero exit status).
+          * 'needed' means the system is able to restart the component,
+            but it is vital part of the service (like auth server). If
+            it fails to start or crashes in less than 10s after the first
+            startup, the system is brought down. If it crashes later on,
+            it is restarted.
+          * 'dispensable' means the component should be running, but if it
+            doesn't start or crashes for some reason, the system simply tries
+            to restart it and keeps running.
+        - `address` is the address on message bus. It is used to ask it to
+            shut down at the end. If you specialize the class for a component
+            that is shut down differently, it might be None.
+        - `params` is a list of parameters to pass to the process when it
+           starts. It is currently unused and this support is left out for
+           now.
+        """
+        if kind not in ['core', 'needed', 'dispensable']:
+            raise ValueError('Component kind can not be ' + kind)
+        self.__state = STATE_STOPPED
+        self._kind = kind
+        self._boss = boss
+        self._process = process
+        self._start_func = None
+        self._address = address
+        self._params = params
+        self._procinfo = None
+
+    def start(self):
+        """
+        Start the component for the first time or restart it. If you need to
+        modify the way a component is started, do not replace this method,
+        but _start_internal. This one does some more bookkeeping around.
+
+        If you try to start an already running component, it raises ValueError.
+        """
+        if self.__state == STATE_DEAD:
+            raise ValueError("Can't resurrect already dead component")
+        if self.running():
+            raise ValueError("Can't start already running component")
+        logger.info(BIND10_COMPONENT_START, self.name())
+        self.__state = STATE_RUNNING
+        self.__start_time = time.time()
+        try:
+            self._start_internal()
+        except Exception as e:
+            logger.error(BIND10_COMPONENT_START_EXCEPTION, self.name(), e)
+            self.failed(None)
+            raise
+
+    def _start_internal(self):
+        """
+        This method does the actual starting of a process. If you need to
+        change the way the component is started, replace this method.
+
+        You can change the "core" of this function by setting self._start_func
+        to a function without parameters. Such function should start the
+        process and return the procinfo object describing the running process.
+
+        If you don't provide the _start_func, the usual startup by calling
+        boss.start_simple is performed.
+
+        If you override the method completely, you should consider overriding
+        pid and _stop_internal (and possibly _failed_internal and name) as well.
+        You should also register any processes started within boss.
+        """
+        # This one is not tested. For one, it starts a real process
+        # which is out of scope of unit tests, for another, it just
+        # delegates the starting to other function in boss (if a derived
+        # class does not provide an override function), which is tested
+        # by use.
+        if self._start_func is not None:
+            procinfo = self._start_func()
+        else:
+            # TODO Handle params, etc
+            procinfo = self._boss.start_simple(self._process)
+        self._procinfo = procinfo
+        self._boss.register_process(self.pid(), self)
+
+    def stop(self):
+        """
+        Stop the component. If you need to modify the way a component is
+        stopped, do not replace this method, but _stop_internal. This one
+        does some more bookkeeping.
+
+        If you try to stop a component that is not running, it raises
+        ValueError.
+        """
+        # This is not tested. It talks with the outher world, which is out
+        # of scope of unittests.
+        if not self.running():
+            raise ValueError("Can't stop a component which is not running")
+        logger.info(BIND10_COMPONENT_STOP, self.name())
+        self.__state = STATE_STOPPED
+        self._stop_internal()
+
+    def _stop_internal(self):
+        """
+        This is the method that does the actual stopping of a component.
+        You can replace this method if you want a different way to do it.
+
+        If you're overriding this one, you probably want to replace the
+        _start_internal and pid methods (and maybe _failed_internal and
+        name as well).
+        """
+        self._boss.stop_process(self._process, self._address)
+        # TODO Some way to wait for the process that doesn't want to
+        # terminate and kill it would prove nice (or add it to boss somewhere?)
+
+    def failed(self, exit_code):
+        """
+        Notify the component it crashed. This will be called from boss object.
+
+        If you try to call failed on a component that is not running,
+        a ValueError is raised.
+
+        If it is a core component or needed component and it was started only
+        recently, the component will become dead and will ask the boss to shut
+        down with error exit status. A dead component can't be started again.
+
+        Otherwise the component will try to restart.
+
+        The exit code is used for logging. It might be None.
+        """
+        logger.error(BIND10_COMPONENT_FAILED, self.name(), self.pid(),
+                     exit_code if exit_code is not None else "unknown")
+        if not self.running():
+            raise ValueError("Can't fail component that isn't running")
+        self.__state = STATE_STOPPED
+        self._failed_internal()
+        # If it is a core component or the needed component failed to start
+        # (including it stopped really soon)
+        if self._kind == 'core' or \
+            (self._kind == 'needed' and time.time() - STARTED_OK_TIME <
+             self.__start_time):
+            self.__state = STATE_DEAD
+            logger.fatal(BIND10_COMPONENT_UNSATISFIED, self.name())
+            self._boss.component_shutdown(1)
+        # This means we want to restart
+        else:
+            logger.warn(BIND10_COMPONENT_RESTART, self.name())
+            self.start()
+
+    def _failed_internal(self):
+        """
+        This method is called from failed. You can replace it if you need
+        some specific behaviour when the component crashes. The default
+        implementation is empty.
+
+        Do not raise exceptions from here, please. The propper shutdown
+        would have not happened.
+        """
+        pass
+
+    def running(self):
+        """
+        Informs if the component is currently running. It assumes the failed
+        is called whenever the component really fails and there might be some
+        time in between actual failure and the call.
+
+        It is not expected for this method to be overriden.
+        """
+        return self.__state == STATE_RUNNING
+
+    def name(self):
+        """
+        Returns human-readable name of the component. This is usually the
+        name of the executable, but it might be something different in a
+        derived class (in case it is overriden).
+        """
+        return self._process
+
+    def pid(self):
+        """
+        Provides a PID of a process, if the component is real running process.
+        This implementation expects it to be a real process, but derived class
+        may return None in case the component is something else.
+
+        This returns None in case it is not yet running.
+
+        You probably want to override this method if you're providing custom
+        _start_internal.
+        """
+        return self._procinfo.pid if self._procinfo else None
+
+    def kill(self, forcefull=False):
+        """
+        The component should be forcefully killed. This does not change the
+        internal state, it just kills the external process and expects a
+        failure to be reported when the process really dies.
+
+        If it isn't running, it does nothing.
+
+        If the forcefull is true, it uses SIGKILL instead of SIGTERM.
+        """
+        if self._procinfo:
+            if forcefull:
+                self._procinfo.process.kill()
+            else:
+                self._procinfo.process.terminate()
+
+class Configurator:
+    """
+    This thing keeps track of configuration changes and starts and stops
+    components as it goes. It also handles the inital startup and final
+    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).
+
+    The parameters are:
+    * `boss`: The boss we are managing for.
+    * `specials`: Dict of specially started components. Each item is a class
+      representing the component.
+
+    The configuration passed to it (by startup() and reconfigure()) is a
+    dictionary, each item represents one component that should be running.
+    The key is an unique identifier used to reference the component. The
+    value is a dictionary describing the component. All items in the
+    description is optional unless told otherwise and they are as follows:
+    * `special` - Some components are started in a special way. If it is
+      present, it specifies which class from the specials parameter should
+      be used to create the component. In that case, some of the following
+      items might be irrelevant, depending on the special component choosen.
+      If it is not there, the basic Component class is used.
+    * `process` - Name of the executable to start. If it is not present,
+      it defaults to the identifier of the component.
+    * `kind` - The kind of component, either of 'core', 'needed' and
+      'dispensable'. This specifies what happens if the component fails.
+      This one is required.
+    * `address` - The address of the component on message bus. It is used
+      to shut down the component. All special components currently either
+      know their own address or don't need one and ignore it. The common
+      components should provide this.
+    * `params` - The command line parameters of the executable. Defaults
+      to no parameters. It is currently unused.
+    * `priority` - When starting the component, the components with higher
+      priority are started before the ones with lower priority. If it is
+      not present, it defaults to 0.
+    """
+    def __init__(self, boss, specials = {}):
+        """
+        Initializes the configurator, but nothing is started yet.
+
+        The boss parameter is the boss object used to start and stop processes.
+        """
+        self.__boss = boss
+        # These could be __private, but as we access them from within unittest,
+        # it's more comfortable to have them just _protected.
+
+        # They are tuples (configuration, component)
+        self._components = {}
+        self._running = False
+        self.__specials = specials
+
+    def __reconfigure_internal(self, old, new):
+        """
+        Does a switch from one configuration to another.
+        """
+        self._run_plan(self._build_plan(old, new))
+
+    def startup(self, configuration):
+        """
+        Starts the first set of processes. This configuration is expected
+        to be hardcoded from the boss itself to start the configuration
+        manager and other similar things.
+        """
+        if self._running:
+            raise ValueError("Trying to start the component configurator " +
+                             "twice")
+        logger.info(BIND10_CONFIGURATOR_START)
+        self.__reconfigure_internal(self._components, configuration)
+        self._running = True
+
+    def shutdown(self):
+        """
+        Shuts everything down.
+        """
+        if not self._running:
+            raise ValueError("Trying to shutdown the component " +
+                             "configurator while it's not yet running")
+        logger.info(BIND10_CONFIGURATOR_STOP)
+        self._running = False
+        self.__reconfigure_internal(self._components, {})
+
+    def reconfigure(self, configuration):
+        """
+        Changes configuration from the current one to the provided. It
+        starts and stops all the components as needed (eg. if there's
+        a component that was not in the original configuration, it is
+        started, any component that was in the old and is not in the
+        new one is stopped).
+        """
+        if not self._running:
+            raise ValueError("Trying to reconfigure the component " +
+                             "configurator while it's not yet running")
+        logger.info(BIND10_CONFIGURATOR_RECONFIGURE)
+        self.__reconfigure_internal(self._components, configuration)
+
+    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.
+        """
+        logger.debug(DBG_TRACE_DATA, BIND10_CONFIGURATOR_BUILD, old, new)
+        plan = []
+        # Handle removals of old components
+        for cname in old.keys():
+            if cname not in new:
+                component = self._components[cname][1]
+                if component.running():
+                    plan.append({
+                        'command': STOP_CMD,
+                        'component': component,
+                        'name': cname
+                    })
+        # Handle transitions of configuration of what is here
+        for cname in new.keys():
+            if cname in old:
+                for option in ['special', 'process', 'kind', 'address',
+                               'params']:
+                    if new[cname].get(option) != old[cname][0].get(option):
+                        raise NotImplementedError('Changing configuration of' +
+                                                  ' a running component is ' +
+                                                  'not yet supported. Remove' +
+                                                  ' and re-add ' + cname +
+                                                  'to get the same effect')
+        # Handle introduction of new components
+        plan_add = []
+        for cname in new.keys():
+            if cname not in old:
+                component_spec = new[cname]
+                creator = Component
+                if 'special' in component_spec:
+                    # TODO: Better error handling
+                    creator = self.__specials[component_spec['special']]
+                component = creator(component_spec.get('process', cname),
+                                    self.__boss, component_spec['kind'],
+                                    component_spec.get('address'),
+                                    component_spec.get('params'))
+                priority = component_spec.get('priority', 0)
+                # We store tuples, priority first, so we can easily sort
+                plan_add.append((priority, {
+                    'component': component,
+                    'command': START_CMD,
+                    'name': cname,
+                    'spec': component_spec
+                }))
+        # Push the starts there sorted by priority
+        plan.extend([command for (_, command) in sorted(plan_add,
+                                                        reverse=True,
+                                                        key=lambda command:
+                                                            command[0])])
+        return plan
+
+    def running(self):
+        """
+        Returns if the configurator is running (eg. was started by startup and
+        not yet stopped by shutdown).
+        """
+        return self._running
+
+    def _run_plan(self, plan):
+        """
+        Run a plan, created beforehand by _build_plan.
+
+        With the start and stop commands, it also adds and removes components
+        in _components.
+
+        Currently implemented commands are:
+        * start
+        * stop
+
+        The plan is a list of tasks, each task is a dictionary. It must contain
+        at last 'component' (a component object to work with) and 'command'
+        (the command to do). Currently, both existing commands need 'name' of
+        the component as well (the identifier from configuration). The 'start'
+        one needs the 'spec' to be there, which is the configuration description
+        of the component.
+        """
+        done = 0
+        try:
+            logger.debug(DBG_TRACE_DATA, BIND10_CONFIGURATOR_RUN, len(plan))
+            for task in plan:
+                component = task['component']
+                command = task['command']
+                logger.debug(DBG_TRACE_DETAILED, BIND10_CONFIGURATOR_TASK,
+                             command, component.name())
+                if command == START_CMD:
+                    component.start()
+                    self._components[task['name']] = (task['spec'], component)
+                elif command == STOP_CMD:
+                    if component.running():
+                        component.stop()
+                    del self._components[task['name']]
+                else:
+                    # Can Not Happen (as the plans are generated by ourselves).
+                    # Therefore not tested.
+                    raise NotImplementedError("Command unknown: " + command)
+                done += 1
+        except:
+            logger.error(BIND10_CONFIGURATOR_PLAN_INTERRUPTED, done, len(plan))
+            raise

+ 98 - 0
src/lib/python/isc/bind10/special_component.py

@@ -0,0 +1,98 @@
+# Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+from isc.bind10.component import Component
+import isc.bind10.sockcreator
+from bind10_config import LIBEXECDIR
+import os
+
+class SockCreator(Component):
+    """
+    The socket creator component. Will start and stop the socket creator
+    accordingly.
+    """
+    def __init__(self, process, boss, kind, address=None, params=None):
+        Component.__init__(self, process, boss, kind)
+        self.__creator = None
+
+    def _start_internal(self):
+        self._boss.curproc = 'b10-sockcreator'
+        self.__creator = isc.bind10.sockcreator.Creator(LIBEXECDIR + ':' +
+                                                        os.environ['PATH'])
+        self._boss.register_process(self.pid(), self)
+
+    def _stop_internal(self):
+        self.__creator.terminate()
+        self.__creator = None
+
+    def pid(self):
+        """
+        Pid of the socket creator. It is provided differently from a usual
+        component.
+        """
+        return self.__creator.pid() if self.__creator else None
+
+    def kill(self, forcefull=False):
+        # We don't really care about forcefull here
+        if self.__creator:
+            self.__creator.kill()
+
+class Msgq(Component):
+    """
+    The message queue. Starting is passed to boss, stopping is not supported
+    and we leave the boss kill it by signal.
+    """
+    def __init__(self, process, boss, kind, address=None, params=None):
+        Component.__init__(self, process, boss, kind)
+        self._start_func = boss.start_msgq
+
+    def _stop_internal(self):
+        pass # Wait for the boss to actually kill it. There's no stop command.
+
+class CfgMgr(Component):
+    def __init__(self, process, boss, kind, address=None, params=None):
+        Component.__init__(self, process, boss, kind, 'ConfigManager')
+        self._start_func = boss.start_cfgmgr
+
+class Auth(Component):
+    def __init__(self, process, boss, kind, address=None, params=None):
+        Component.__init__(self, process, boss, kind, 'Auth')
+        self._start_func = boss.start_auth
+
+class Resolver(Component):
+    def __init__(self, process, boss, kind, address=None, params=None):
+        Component.__init__(self, process, boss, kind, 'Resolver')
+        self._start_func = boss.start_resolver
+
+class CmdCtl(Component):
+    def __init__(self, process, boss, kind, address=None, params=None):
+        Component.__init__(self, process, boss, kind, 'Cmdctl')
+        self._start_func = boss.start_cmdctl
+
+def get_specials():
+    """
+    List of specially started components. Each one should be the class than can
+    be created for that component.
+    """
+    return {
+        'sockcreator': SockCreator,
+        'msgq': Msgq,
+        'cfgmgr': CfgMgr,
+        # TODO: Should these be replaced by configuration in config manager only?
+        # They should not have any parameters anyway
+        'auth': Auth,
+        'resolver': Resolver,
+        'cmdctl': CmdCtl
+    }

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

@@ -1,7 +1,7 @@
 PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
 #PYTESTS = args_test.py bind10_test.py
 # NOTE: this has a generated test found in the builddir
-PYTESTS = sockcreator_test.py
+PYTESTS = sockcreator_test.py component_test.py
 
 EXTRA_DIST = $(PYTESTS)
 

+ 829 - 0
src/lib/python/isc/bind10/tests/component_test.py

@@ -0,0 +1,829 @@
+# Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""
+Tests for the isc.bind10.component module and the
+isc.bind10.special_component module.
+"""
+
+import unittest
+import isc.log
+import time
+import copy
+from isc.bind10.component import Component, Configurator
+import isc.bind10.special_component
+
+class TestError(Exception):
+    """
+    Just a private exception not known to anybody we use for our tests.
+    """
+    pass
+
+class BossUtils:
+    """
+    A class that brings some utilities for pretending we're Boss.
+    This is expected to be inherited by the testcases themselves.
+    """
+    def setUp(self):
+        """
+        Part of setup. Should be called by descendant's setUp.
+        """
+        self._shutdown = False
+        self._exitcode = None
+        # Back up the time function, we may want to replace it with something
+        self.__orig_time = isc.bind10.component.time.time
+
+    def tearDown(self):
+        """
+        Clean up after tests. If the descendant implements a tearDown, it
+        should call this method internally.
+        """
+        # Return the original time function
+        isc.bind10.component.time.time = self.__orig_time
+
+    def component_shutdown(self, exitcode=0):
+        """
+        Mock function to shut down. We just note we were asked to do so.
+        """
+        self._shutdown = True
+        self._exitcode = exitcode
+
+    def _timeskip(self):
+        """
+        Skip in time to future some 30s. Implemented by replacing the
+        time.time function in the tested module with function that returns
+        current time increased by 30.
+        """
+        tm = time.time()
+        isc.bind10.component.time.time = lambda: tm + 30
+
+    # Few functions that pretend to start something. Part of pretending of
+    # being boss.
+    def start_msgq(self):
+        pass
+
+    def start_cfgmgr(self):
+        pass
+
+    def start_auth(self):
+        pass
+
+    def start_resolver(self):
+        pass
+
+    def start_cmdctl(self):
+        pass
+
+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):
+        """
+        Mock function, installed into the component into _start_internal.
+        This only notes the component was "started".
+        """
+        self.__start_called = True
+
+    def __stop(self):
+        """
+        Mock function, installed into the component into _stop_internal.
+        This only notes the component was "stopped".
+        """
+        self.__stop_called = True
+
+    def __fail(self):
+        """
+        Mock function, installed into the component into _failed_internal.
+        This only notes the component called the method.
+        """
+        self.__failed_called = True
+
+    def __fail_to_start(self):
+        """
+        Mock function. It can be installed into the component's _start_internal
+        to simulate a component that fails to start by raising an exception.
+        """
+        orig_started = self.__start_called
+        self.__start_called = True
+        if not orig_started:
+            # This one is from restart. Avoid infinite recursion for now.
+            # FIXME: We should use the restart scheduler to avoid it, not this.
+            raise TestError("Test error")
+
+    def __create_component(self, kind):
+        """
+        Convenience function that creates a component of given kind
+        and installs the mock functions into it so we can hook up into
+        its behaviour.
+
+        The process used is some nonsense, as this isn't used in this
+        kind of tests and we pretend to be the boss.
+        """
+        component = Component('No process', self, kind, 'homeless', [])
+        component._start_internal = self.__start
+        component._stop_internal = self.__stop
+        component._failed_internal = self.__fail
+        return component
+
+    def test_name(self):
+        """
+        Test the name provides whatever we passed to the constructor as process.
+        """
+        component = self.__create_component('core')
+        self.assertEqual('No process', component.name())
+
+    def test_guts(self):
+        """
+        Test the correct data are stored inside the component.
+        """
+        component = self.__create_component('core')
+        self.assertEqual(self, component._boss)
+        self.assertEqual("No process", component._process)
+        self.assertEqual(None, component._start_func)
+        self.assertEqual("homeless", component._address)
+        self.assertEqual([], component._params)
+
+    def __check_startup(self, component):
+        """
+        Check that nothing was called yet. A newly created component should
+        not get started right away, so this should pass after the creation.
+        """
+        self.assertFalse(self._shutdown)
+        self.assertFalse(self.__start_called)
+        self.assertFalse(self.__stop_called)
+        self.assertFalse(self.__failed_called)
+        self.assertFalse(component.running())
+        # We can't stop or fail the component yet
+        self.assertRaises(ValueError, component.stop)
+        self.assertRaises(ValueError, component.failed, 1)
+
+    def __check_started(self, component):
+        """
+        Check the component was started, but not stopped anyhow yet.
+        """
+        self.assertFalse(self._shutdown)
+        self.assertTrue(self.__start_called)
+        self.assertFalse(self.__stop_called)
+        self.assertFalse(self.__failed_called)
+        self.assertTrue(component.running())
+
+    def __check_dead(self, component):
+        """
+        Check the component is completely dead, and the server too.
+        """
+        self.assertTrue(self._shutdown)
+        self.assertTrue(self.__start_called)
+        self.assertFalse(self.__stop_called)
+        self.assertTrue(self.__failed_called)
+        self.assertEqual(1, self._exitcode)
+        self.assertFalse(component.running())
+        # Surely it can't be stopped when already dead
+        self.assertRaises(ValueError, component.stop)
+        # Nor started
+        self.assertRaises(ValueError, component.start)
+        # Nor it can fail again
+        self.assertRaises(ValueError, component.failed, 1)
+
+    def __check_restarted(self, component):
+        """
+        Check the component restarted successfully.
+
+        Currently, it is implemented as starting it again right away. This will
+        change, it will register itself into the restart schedule in boss. But
+        as the integration with boss is not clear yet, we don't know how
+        exactly that will happen.
+
+        Reset the self.__start_called to False before calling the function when
+        the component should fail.
+        """
+        self.assertFalse(self._shutdown)
+        self.assertTrue(self.__start_called)
+        self.assertFalse(self.__stop_called)
+        self.assertTrue(self.__failed_called)
+        self.assertTrue(component.running())
+        # Check it can't be started again
+        self.assertRaises(ValueError, component.start)
+
+    def __do_start_stop(self, kind):
+        """
+        This is a body of a test. It creates a component of given kind,
+        then starts it and stops it. It checks correct functions are called
+        and the component's status is correct.
+
+        It also checks the component can't be started/stopped twice.
+        """
+        # Create it and check it did not do any funny stuff yet
+        component = self.__create_component(kind)
+        self.__check_startup(component)
+        # Start it and check it called the correct starting functions
+        component.start()
+        self.__check_started(component)
+        # Check it can't be started twice
+        self.assertRaises(ValueError, component.start)
+        # Stop it again and check
+        component.stop()
+        self.assertFalse(self._shutdown)
+        self.assertTrue(self.__start_called)
+        self.assertTrue(self.__stop_called)
+        self.assertFalse(self.__failed_called)
+        self.assertFalse(component.running())
+        # Check it can't be stopped twice
+        self.assertRaises(ValueError, component.stop)
+        # Or failed
+        self.assertRaises(ValueError, component.failed, 1)
+        # But it can be started again if it is stopped
+        # (no more checking here, just it doesn't crash)
+        component.start()
+
+    def test_start_stop_core(self):
+        """
+        A start-stop test for core component. See do_start_stop.
+        """
+        self.__do_start_stop('core')
+
+    def test_start_stop_needed(self):
+        """
+        A start-stop test for needed component. See do_start_stop.
+        """
+        self.__do_start_stop('needed')
+
+    def test_start_stop_dispensable(self):
+        """
+        A start-stop test for dispensable component. See do_start_stop.
+        """
+        self.__do_start_stop('dispensable')
+
+    def test_start_fail_core(self):
+        """
+        Start and then fail a core component. It should stop the whole server.
+        """
+        # Just ordinary startup
+        component = self.__create_component('core')
+        self.__check_startup(component)
+        component.start()
+        self.__check_started(component)
+        # Pretend the component died
+        component.failed(1)
+        # It should bring down the whole server
+        self.__check_dead(component)
+
+    def test_start_fail_core_later(self):
+        """
+        Start and then fail a core component, but let it be running for longer time.
+        It should still stop the whole server.
+        """
+        # Just ordinary startup
+        component = self.__create_component('core')
+        self.__check_startup(component)
+        component.start()
+        self.__check_started(component)
+        self._timeskip()
+        # Pretend the component died some time later
+        component.failed(1)
+        # Check the component is still dead
+        self.__check_dead(component)
+
+    def test_start_fail_needed(self):
+        """
+        Start and then fail a needed component. As this happens really soon after
+        being started, it is considered failure to start and should bring down the
+        whole server.
+        """
+        # Just ordinary startup
+        component = self.__create_component('needed')
+        self.__check_startup(component)
+        component.start()
+        self.__check_started(component)
+        # Make it fail right away.
+        component.failed(1)
+        self.__check_dead(component)
+
+    def test_start_fail_needed_later(self):
+        """
+        Start and then fail a needed component. But the failure is later on, so
+        we just restart it and will be happy.
+        """
+        # Just ordinary startup
+        component = self.__create_component('needed')
+        self.__check_startup(component)
+        component.start()
+        self.__check_started(component)
+        # Make it fail later on
+        self.__start_called = False
+        self._timeskip()
+        component.failed(1)
+        self.__check_restarted(component)
+
+    def test_start_fail_dispensable(self):
+        """
+        Start and then fail a dispensable component. Should just get restarted.
+        """
+        # Just ordinary startup
+        component = self.__create_component('needed')
+        self.__check_startup(component)
+        component.start()
+        self.__check_started(component)
+        # Make it fail right away
+        self.__start_called = False
+        component.failed(1)
+        self.__check_restarted(component)
+
+    def test_start_fail_dispensable(self):
+        """
+        Start and then later on fail a dispensable component. Should just get
+        restarted.
+        """
+        # Just ordinary startup
+        component = self.__create_component('needed')
+        self.__check_startup(component)
+        component.start()
+        self.__check_started(component)
+        # Make it fail later on
+        self.__start_called = False
+        self._timeskip()
+        component.failed(1)
+        self.__check_restarted(component)
+
+    def test_fail_core(self):
+        """
+        Failure to start a core component. Should bring the system down
+        and the exception should get through.
+        """
+        component = self.__create_component('core')
+        self.__check_startup(component)
+        component._start_internal = self.__fail_to_start
+        self.assertRaises(TestError, component.start)
+        self.__check_dead(component)
+
+    def test_fail_needed(self):
+        """
+        Failure to start a needed component. Should bring the system down
+        and the exception should get through.
+        """
+        component = self.__create_component('needed')
+        self.__check_startup(component)
+        component._start_internal = self.__fail_to_start
+        self.assertRaises(TestError, component.start)
+        self.__check_dead(component)
+
+    def test_fail_dispensable(self):
+        """
+        Failure to start a dispensable component. The exception should get
+        through, but it should be restarted.
+        """
+        component = self.__create_component('dispensable')
+        self.__check_startup(component)
+        component._start_internal = self.__fail_to_start
+        self.assertRaises(TestError, component.start)
+        self.__check_restarted(component)
+
+    def test_bad_kind(self):
+        """
+        Test the component rejects nonsensical kinds. This includes bad
+        capitalization.
+        """
+        for kind in ['Core', 'CORE', 'nonsense', 'need ed', 'required']:
+            self.assertRaises(ValueError, Component, 'No process', self, kind)
+
+    def test_pid_not_running(self):
+        """
+        Test that a componet that is not yet started doesn't have a PID.
+        But it won't failed if asked for and returns None.
+        """
+        for component_type in [Component,
+                               isc.bind10.special_component.SockCreator,
+                               isc.bind10.special_component.Msgq,
+                               isc.bind10.special_component.CfgMgr,
+                               isc.bind10.special_component.Auth,
+                               isc.bind10.special_component.Resolver,
+                               isc.bind10.special_component.CmdCtl]:
+            component = component_type('none', self, 'needed')
+            self.assertIsNone(component.pid())
+
+    def test_kill_unstarted(self):
+        """
+        Try to kill the component if it's not started. Should not fail.
+
+        We do not try to kill a running component, as we should not start
+        it during unit tests.
+        """
+        component = Component(self, 'component', 'needed')
+        component.kill()
+        component.kill(True)
+
+class TestComponent(Component):
+    """
+    A test component. It does not start any processes or so, it just logs
+    information about what happens.
+    """
+    def __init__(self, owner, name, kind, address=None, params=None):
+        """
+        Initializes the component. The owner is the test that started the
+        component. The logging will happen into it.
+
+        The process is used as a name for the logging.
+        """
+        Component.__init__(self, name, owner, kind, address, params)
+        self.__owner = owner
+        self.__name = name
+        self.log('init')
+        self.log(kind)
+
+    def log(self, event):
+        """
+        Log an event into the owner. The owner can then check the correct
+        order of events that happened.
+        """
+        self.__owner.log.append((self.__name, event))
+
+    def _start_internal(self):
+        self.log('start')
+
+    def _stop_internal(self):
+        self.log('stop')
+
+    def _failed_internal(self):
+        self.log('failed')
+
+    def kill(self, forcefull=False):
+        self.log('killed')
+
+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.
+    """
+    def setUp(self):
+        """
+        Prepare some test data for the tests.
+        """
+        BossUtils.setUp(self)
+        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. They 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
+        self.__specials = { 'test': self.__component_test }
+
+    def __component_test(self, process, boss, kind, address=None, params=None):
+        """
+        Create a test component. It will log events to us.
+        """
+        self.assertEqual(self, boss)
+        return TestComponent(self, process, kind, address, params)
+
+    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.__specials)
+        self.assertEqual([], self.log)
+        self.assertEqual({}, configurator._components)
+        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, self.__specials)
+        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',
+                'spec': {'a': 1}
+            },
+            {
+                'component': started,
+                'command': 'stop',
+                'name': 'second',
+                'spec': {}
+            },
+            {
+                'component': FailComponent('third', self, 'needed'),
+                'command': 'start',
+                'name': 'third',
+                'spec': {}
+            },
+            {
+                'component': self.__component_test('fourth', self, 'core'),
+                'command': 'start',
+                'name': 'fourth',
+                'spec': {}
+            }
+        ]
+        # 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': ({'a': 1}, stopped)},
+                         configurator._components)
+
+    def __build_components(self, config):
+        """
+        Insert the components into the configuration to specify possible
+        Configurator._components.
+
+        Actually, the components are None, but we need something to be there.
+        """
+        result = {}
+        for name in config.keys():
+            result[name] = (config[name], None)
+        return result
+
+    def test_build_plan(self):
+        """
+        Test building the plan correctly. Not complete yet, this grows as we
+        add more ways of changing the plan.
+        """
+        configurator = Configurator(self, self.__specials)
+        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'])
+            component = task['component']
+            self.assertIsNone(component._address)
+            self.assertIsNone(component._params)
+
+        # A plan to go from older state to newer one containing more components
+        bigger = copy.copy(self.__core)
+        bigger['additional'] = {
+            'priority': 6,
+            'special': 'test',
+            'process': 'additional',
+            'kind': 'needed'
+        }
+        self.log = []
+        plan = configurator._build_plan(self.__build_components(self.__core),
+                                        bigger)
+        self.assertEqual([('additional', 'init'), ('additional', 'needed')],
+                         self.log)
+        self.assertEqual(1, len(plan))
+        self.assertTrue('component' in plan[0])
+        component = plan[0]['component']
+        self.assertEqual('start', plan[0]['command'])
+        self.assertEqual('additional', plan[0]['name'])
+
+        # Now remove the one component again
+        # We run the plan so the component is wired into internal structures
+        configurator._run_plan(plan)
+        self.log = []
+        plan = configurator._build_plan(self.__build_components(bigger),
+                                        self.__core)
+        self.assertEqual([], self.log)
+        self.assertEqual([{
+            'command': 'stop',
+            'name': 'additional',
+            'component': component
+        }], plan)
+
+        # We want to switch a component. So, prepare the configurator so it
+        # holds one
+        configurator._run_plan(configurator._build_plan(
+             self.__build_components(self.__core), bigger))
+        # Get a different configuration with a different component
+        different = copy.copy(self.__core)
+        different['another'] = {
+            'special': 'test',
+            'process': 'another',
+            'kind': 'dispensable'
+        }
+        self.log = []
+        plan = configurator._build_plan(self.__build_components(bigger),
+                                        different)
+        self.assertEqual([('another', 'init'), ('another', 'dispensable')],
+                         self.log)
+        self.assertEqual(2, len(plan))
+        self.assertEqual('stop', plan[0]['command'])
+        self.assertEqual('additional', plan[0]['name'])
+        self.assertTrue('component' in plan[0])
+        self.assertEqual('start', plan[1]['command'])
+        self.assertEqual('another', plan[1]['name'])
+        self.assertTrue('component' in plan[1])
+
+        # Some slightly insane plans, like missing process, having parameters,
+        # no special, etc
+        plan = configurator._build_plan({}, {
+            'component': {
+                'kind': 'needed',
+                'params': ["1", "2"],
+                'address': 'address'
+            }
+        })
+        self.assertEqual(1, len(plan))
+        self.assertEqual('start', plan[0]['command'])
+        self.assertEqual('component', plan[0]['name'])
+        component = plan[0]['component']
+        self.assertEqual('component', component.name())
+        self.assertEqual(["1", "2"], component._params)
+        self.assertEqual('address', component._address)
+        self.assertEqual('needed', component._kind)
+        # We don't use isinstance on purpose, it would allow a descendant
+        self.assertTrue(type(component) is Component)
+        plan = configurator._build_plan({}, {
+            'component': { 'kind': 'dispensable' }
+        })
+        self.assertEqual(1, len(plan))
+        self.assertEqual('start', plan[0]['command'])
+        self.assertEqual('component', plan[0]['name'])
+        component = plan[0]['component']
+        self.assertEqual('component', component.name())
+        self.assertIsNone(component._params)
+        self.assertIsNone(component._address)
+        self.assertEqual('dispensable', component._kind)
+
+    def __do_switch(self, option, value):
+        """
+        Start it with some component and then switch the configuration of the
+        component. This will probably raise, as it is not yet supported.
+        """
+        configurator = Configurator(self, self.__specials)
+        compconfig = {
+            'special': 'test',
+            'process': 'process',
+            'priority': 13,
+            'kind': 'core'
+        }
+        modifiedconfig = copy.copy(compconfig)
+        modifiedconfig[option] = value
+        return configurator._build_plan({'comp': (compconfig, None)},
+                                        {'comp': modifiedconfig})
+
+    def test_change_config_plan(self):
+        """
+        Test changing a configuration of one component. This is not yet
+        implemented and should therefore throw.
+        """
+        self.assertRaises(NotImplementedError, self.__do_switch, 'kind',
+                          'dispensable')
+        self.assertRaises(NotImplementedError, self.__do_switch, 'special',
+                          'not_a_test')
+        self.assertRaises(NotImplementedError, self.__do_switch, 'process',
+                          'different')
+        self.assertRaises(NotImplementedError, self.__do_switch, 'address',
+                          'different')
+        self.assertRaises(NotImplementedError, self.__do_switch, 'params',
+                          ['different'])
+        # This does not change anything on running component, so no need to
+        # raise
+        self.assertEqual([], self.__do_switch('priority', 5))
+        # Check against false positive, if the data are the same, but different
+        # instance
+        self.assertEqual([], self.__do_switch('special', 'test'))
+
+    def __check_shutdown_log(self):
+        """
+        Checks the log for shutting down from the core configuration.
+        """
+        # We know everything must be stopped, we know what it is.
+        # But we don't know the order, so we check everything is exactly
+        # once in the log
+        components = set(self.__core.keys())
+        for (name, command) in self.log:
+            self.assertEqual('stop', command)
+            self.assertTrue(name in components)
+            components.remove(name)
+        self.assertEqual(set([]), components, "Some component wasn't stopped")
+
+    def test_run(self):
+        """
+        Passes some configuration to the startup method and sees if
+        the components are started up. Then it reconfigures it with
+        empty configuration, the original configuration again and shuts
+        down.
+
+        It also checks the components are kept inside the configurator.
+        """
+        configurator = Configurator(self, self.__specials)
+        # Can't reconfigure nor stop yet
+        self.assertRaises(ValueError, configurator.reconfigure, self.__core)
+        self.assertRaises(ValueError, configurator.shutdown)
+        self.assertFalse(configurator.running())
+        # Start it
+        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[core],
+                             configurator._components[core][0])
+        self.assertEqual(set(self.__core), set(configurator._components))
+        self.assertTrue(configurator.running())
+        # It can't be started twice
+        self.assertRaises(ValueError, configurator.startup, self.__core)
+
+        self.log = []
+        # Reconfigure - stop everything
+        configurator.reconfigure({})
+        self.assertEqual({}, configurator._components)
+        self.assertTrue(configurator.running())
+        self.__check_shutdown_log()
+
+        # Start it again
+        self.log = []
+        configurator.reconfigure(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[core],
+                             configurator._components[core][0])
+        self.assertEqual(set(self.__core), set(configurator._components))
+        self.assertTrue(configurator.running())
+
+        # Do a shutdown
+        self.log = []
+        configurator.shutdown()
+        self.assertEqual({}, configurator._components)
+        self.assertFalse(configurator.running())
+        self.__check_shutdown_log()
+
+        # It can't be stopped twice
+        self.assertRaises(ValueError, configurator.shutdown)
+
+    def test_sort_no_prio(self):
+        """
+        There was a bug if there were two things with the same priority
+        (or without priority), it failed as it couldn't compare the dicts
+        there. This tests it doesn't crash.
+        """
+        configurator = Configurator(self, self.__specials)
+        configurator._build_plan({}, {
+                                         "c1": { 'kind': 'dispensable'},
+                                         "c2": { 'kind': 'dispensable'}
+                                     })
+
+if __name__ == '__main__':
+    isc.log.init("bind10") # FIXME Should this be needed?
+    isc.log.resetUnitTestRootLogger()
+    unittest.main()