diff options
-rw-r--r-- | debian/control | 11 | ||||
-rwxr-xr-x | debian/rules | 7 | ||||
-rw-r--r-- | doc/development/core.txt | 5 | ||||
-rw-r--r-- | doc/server/configuration.txt | 37 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options.py | 13 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Admin/Client.py | 1 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Core.py | 4 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/MultiprocessingCore.py | 204 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugins/Metadata.py | 6 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugins/Packages/__init__.py | 5 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/SSLServer.py | 9 | ||||
-rwxr-xr-x | src/sbin/bcfg2-server | 31 |
12 files changed, 292 insertions, 41 deletions
diff --git a/debian/control b/debian/control index 6c7278e4e..20cef93c8 100644 --- a/debian/control +++ b/debian/control @@ -4,25 +4,24 @@ Priority: optional Maintainer: Arto Jantunen <viiru@debian.org> Uploaders: Sami Haahtinen <ressu@debian.org> Build-Depends: debhelper (>= 7.0.50~), - python (>= 2.3.5-7), + python (>= 2.6), python-setuptools, python-sphinx (>= 1.0.7+dfsg) | python3-sphinx, python-lxml, python-daemon, python-cherrypy, + python-gamin, python-pyinotify, python-m2crypto, python-doc, python-mock-doc Build-Depends-Indep: python-support (>= 0.5.3) Standards-Version: 3.8.0.0 -XS-Python-Version: >= 2.3 Homepage: http://bcfg2.org/ Package: bcfg2 Architecture: all -Depends: ${python:Depends}, ${misc:Depends}, debsums, python-apt, ucf, lsb-base (>= 3.1-9), python-m2crypto | python-ssl | python2.6 | python3.0 | python3.1 | python3.2 -XB-Python-Version: >= 2.3 +Depends: ${python:Depends}, ${misc:Depends}, debsums, python-apt, ucf, lsb-base (>= 3.1-9), python (>= 2.6) Description: Configuration management client Bcfg2 is a configuration management system that generates configuration sets for clients bound by client profiles. @@ -31,8 +30,7 @@ Description: Configuration management client Package: bcfg2-server Architecture: all -Depends: ${python:Depends}, ${misc:Depends}, python-lxml (>= 0.9), libxml2-utils (>= 2.6.23), lsb-base (>= 3.1-9), ucf, bcfg2 (= ${binary:Version}), openssl, python-ssl | python2.6 | python3.0 | python3.1 | python3.2, python-pyinotify | python-gamin, python-daemon -XB-Python-Version: >= 2.4 +Depends: ${python:Depends}, ${misc:Depends}, python-lxml (>= 0.9), libxml2-utils (>= 2.6.23), lsb-base (>= 3.1-9), ucf, bcfg2 (= ${binary:Version}), openssl, python (>= 2.6), python-pyinotify | python-gamin, python-daemon Recommends: graphviz, patch Suggests: python-cheetah, python-genshi (>= 0.4.4), python-profiler, python-sqlalchemy (>= 0.5.0), python-django, mail-transport-agent, bcfg2-doc (= ${binary:Version}) Description: Configuration management server @@ -45,7 +43,6 @@ Package: bcfg2-web Architecture: all Depends: ${python:Depends}, ${misc:Depends}, bcfg2-server (= ${binary:Version}), python-django, Suggests: python-mysqldb, python-psycopg2, python-sqlite, libapache2-mod-wsgi -XB-Python-Version: >= 2.4 Description: Configuration management web interface Bcfg2 is a configuration management system that generates configuration sets for clients bound by client profiles. diff --git a/debian/rules b/debian/rules index fcbf6346c..5694e4e37 100755 --- a/debian/rules +++ b/debian/rules @@ -3,13 +3,6 @@ %: dh $@ --with python-support,sphinxdoc -override_dh_auto_install: - # Make the build destination dir consistent between pre-7.3 and 7.3 and - # later debhelper - see http://bcfg2.org/ticket/791 - dh_auto_install - test -d debian/tmp/usr/local && mv debian/tmp/usr/local/* debian/tmp/usr || exit 0 - test -d debian/tmp/usr/local && rmdir debian/tmp/usr/local || exit 0 - override_dh_installinit: # Install bcfg2 initscript without starting it on postinst dh_installinit --package=bcfg2 --no-start diff --git a/doc/development/core.txt b/doc/development/core.txt index 205eb5c59..3953d3402 100644 --- a/doc/development/core.txt +++ b/doc/development/core.txt @@ -71,6 +71,11 @@ XML-RPC Server .. automodule:: Bcfg2.Server.SSLServer +Multiprocessing Core +-------------------- + +.. automodule:: Bcfg2.Server.MultiprocessingCore + CherryPy Core ------------- diff --git a/doc/server/configuration.txt b/doc/server/configuration.txt index be421207c..f93b172ef 100644 --- a/doc/server/configuration.txt +++ b/doc/server/configuration.txt @@ -20,6 +20,9 @@ Bcfg2, although it has become easier in 1.3.0. The steps to do so are described in three sections below: Common steps for all versions; steps for older versions only; and steps for 1.3.0. +Many of the steps below may have already been performed by your OS +packages. + Common Steps ------------ @@ -129,7 +132,7 @@ is even invoked. Restart ``bcfg2-server`` and you should see it running as non-root in ``ps`` output:: - % ps -ef | grep '[b]cfg2-server' + % ps -ef | grep '[b]cfg2-server' 1000 11581 1 0 07:55 ? 00:00:15 python usr/sbin/bcfg2-server -C /etc/bcfg2.conf -D /var/run/bcfg2-server/bcfg2-server.pid Steps on Bcfg2 1.3.0 @@ -157,7 +160,7 @@ natively in 1.3. Simply add the following lines to ``bcfg2.conf``:: Restart ``bcfg2-server`` and you should see it running as non-root in ``ps`` output:: - % ps -ef | grep '[b]cfg2-server' + % ps -ef | grep '[b]cfg2-server' 1000 11581 1 0 07:55 ? 00:00:15 python usr/sbin/bcfg2-server -C /etc/bcfg2.conf -D /var/run/bcfg2-server/bcfg2-server.pid .. _server-backends: @@ -167,10 +170,11 @@ Server Backends .. versionadded:: 1.3.0 -Bcfg2 supports two different server backends: a builtin server -based on the Python SimpleXMLRPCServer object, and a server that uses -CherryPy (http://www.cherrypy.org). Each one has advantages and -disadvantages. +Bcfg2 supports three different server backends: a builtin server based +on the Python SimpleXMLRPCServer object; a server that uses CherryPy +(http://www.cherrypy.org); and a version of the builtin server that +uses the Python :mod:`multiprocessing` module. Each one has +advantages and disadvantages. The builtin server: @@ -179,27 +183,36 @@ The builtin server: * Works on Python 2.4; * Is slow with larger numbers of clients. +The multiprocessing server: + +* Leverages most of the stability and maturity of the builtin server, + but does have some new bits; +* Introduces concurrent processing to Bcfg2, which may break in + various edge cases; +* Supports certificate authentication; +* Requires Python 2.6; +* Is faster with large numbers of concurrent runs. + The CherryPy server: * Is very new and potentially buggy; * Does not support certificate authentication yet, only password authentication; -* Requires CherryPy 3.2, which requires Python 2.5; +* Requires CherryPy 3.3, which requires Python 2.5; * Is smarter about daemonization, particularly if you are :ref:`server-dropping-privs`; * Is faster with large numbers of clients. Basically, the builtin server should be used unless you have a -particular need for performance, and can sacrifice certificate -authentication. +particular need for performance. The CherryPy server is purely +experimental at this point. To select which backend to use, set the ``backend`` option in the ``[server]`` section of ``/etc/bcfg2.conf``. Options are: * ``cherrypy`` * ``builtin`` +* ``multiprocessing`` * ``best`` (the default; currently the same as ``builtin``) -If the certificate authentication issues (a limitation in CherryPy -itself) can be resolved and the CherryPy server proves to be stable, -it will likely become the default (and ``best``) in a future release. +``best`` may change in future releases. diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py index 67dcf901e..7e7232820 100644 --- a/src/lib/Bcfg2/Options.py +++ b/src/lib/Bcfg2/Options.py @@ -603,6 +603,16 @@ SERVER_AUTHENTICATION = \ default='cert+password', odesc='{cert|bootstrap|cert+password}', cf=('communication', 'authentication')) +SERVER_CHILDREN = \ + Option('Spawn this number of children for the multiprocessing core. ' + 'By default spawns children equivalent to the number of processors ' + 'in the machine.', + default=None, + cmd='--children', + odesc='<children>', + cf=('server', 'children'), + cook=get_int, + long_arg=True) # database options DB_ENGINE = \ @@ -1109,7 +1119,8 @@ SERVER_COMMON_OPTIONS = dict(repo=SERVER_REPOSITORY, vcs_root=SERVER_VCS_ROOT, authentication=SERVER_AUTHENTICATION, perflog=LOG_PERFORMANCE, - perflog_interval=PERFLOG_INTERVAL) + perflog_interval=PERFLOG_INTERVAL, + children=SERVER_CHILDREN) CRYPT_OPTIONS = dict(encrypt=ENCRYPT, decrypt=DECRYPT, diff --git a/src/lib/Bcfg2/Server/Admin/Client.py b/src/lib/Bcfg2/Server/Admin/Client.py index b7916fab9..570e993ed 100644 --- a/src/lib/Bcfg2/Server/Admin/Client.py +++ b/src/lib/Bcfg2/Server/Admin/Client.py @@ -8,6 +8,7 @@ from Bcfg2.Server.Plugin import MetadataConsistencyError class Client(Bcfg2.Server.Admin.MetadataCore): """ Create, delete, or list client entries """ __usage__ = "[options] [add|del|list] [attr=val]" + __plugin_whitelist__ = ["Metadata"] def __call__(self, args): if len(args) == 0: diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py index d61543256..8ef9e3e96 100644 --- a/src/lib/Bcfg2/Server/Core.py +++ b/src/lib/Bcfg2/Server/Core.py @@ -301,6 +301,7 @@ class BaseCore(object): self.logger.info("Performance statistics: " "%s min=%.06f, max=%.06f, average=%.06f, " "count=%d" % ((name, ) + stats)) + self.logger.debug("Performance logging thread terminated") def _file_monitor_thread(self): """ The thread that runs the @@ -321,6 +322,7 @@ class BaseCore(object): except: continue self._update_vcs_revision() + self.logger.debug("File monitor thread terminated") @Bcfg2.Server.Statistics.track_statistics() def _update_vcs_revision(self): @@ -441,8 +443,10 @@ class BaseCore(object): if not self.terminate.isSet(): self.terminate.set() self.fam.shutdown() + self.logger.debug("FAM shut down") for plugin in list(self.plugins.values()): plugin.shutdown() + self.logger.debug("All plugins shut down") @property def metadata_cache_mode(self): diff --git a/src/lib/Bcfg2/Server/MultiprocessingCore.py b/src/lib/Bcfg2/Server/MultiprocessingCore.py new file mode 100644 index 000000000..81fba7092 --- /dev/null +++ b/src/lib/Bcfg2/Server/MultiprocessingCore.py @@ -0,0 +1,204 @@ +""" The multiprocessing server core is a reimplementation of the +:mod:`Bcfg2.Server.BuiltinCore` that uses the Python +:mod:`multiprocessing` library to offload work to multiple child +processes. As such, it requires Python 2.6+. +""" + +import threading +import lxml.etree +import multiprocessing +from Bcfg2.Compat import Queue +from Bcfg2.Server.Core import BaseCore, exposed +from Bcfg2.Server.BuiltinCore import Core as BuiltinCore + + +class DualEvent(object): + """ DualEvent is a clone of :class:`threading.Event` that + internally implements both :class:`threading.Event` and + :class:`multiprocessing.Event`. """ + + def __init__(self, threading_event=None, multiprocessing_event=None): + self._threading_event = threading_event or threading.Event() + self._multiproc_event = multiprocessing_event or \ + multiprocessing.Event() + if threading_event or multiprocessing_event: + # initialize internal flag to false, regardless of the + # state of either object passed in + self.clear() + + def is_set(self): + """ Return true if and only if the internal flag is true. """ + return self._threading_event.is_set() + + isSet = is_set + + def set(self): + """ Set the internal flag to true. """ + self._threading_event.set() + self._multiproc_event.set() + + def clear(self): + """ Reset the internal flag to false. """ + self._threading_event.clear() + self._multiproc_event.clear() + + def wait(self, timeout=None): + """ Block until the internal flag is true, or until the + optional timeout occurs. """ + return self._threading_event.wait(timeout=timeout) + + +class ChildCore(BaseCore): + """ A child process for :class:`Bcfg2.MultiprocessingCore.Core`. + This core builds configurations from a given + :class:`multiprocessing.Pipe`. Note that this is a full-fledged + server core; the only input it gets from the parent process is the + hostnames of clients to render. All other state comes from the + FAM. However, this core only is used to render configs; it doesn't + handle anything else (authentication, probes, etc.) because those + are all much faster. There's no reason that it couldn't handle + those, though, if the pipe communication "protocol" were made more + robust. """ + + #: How long to wait while polling for new clients to build. This + #: doesn't affect the speed with which a client is built, but + #: setting it too high will result in longer shutdown times, since + #: we only check for the termination event from the main process + #: every ``poll_wait`` seconds. + poll_wait = 5.0 + + def __init__(self, setup, pipe, terminate): + """ + :param setup: A Bcfg2 options dict + :type setup: Bcfg2.Options.OptionParser + :param pipe: The pipe to which client hostnames are added for + ChildCore objects to build configurations, and to + which client configurations are added after + having been built by ChildCore objects. + :type pipe: multiprocessing.Pipe + :param terminate: An event that flags ChildCore objects to shut + themselves down. + :type terminate: multiprocessing.Event + """ + BaseCore.__init__(self, setup) + + #: The pipe to which client hostnames are added for ChildCore + #: objects to build configurations, and to which client + #: configurations are added after having been built by + #: ChildCore objects. + self.pipe = pipe + + #: The :class:`multiprocessing.Event` that will be monitored + #: to determine when this child should shut down. + self.terminate = terminate + + def _daemonize(self): + return True + + def _run(self): + return True + + def _block(self): + while not self.terminate.isSet(): + try: + if self.pipe.poll(self.poll_wait): + if not self.metadata.use_database: + # handle FAM events, in case (for instance) the + # client has just been added to clients.xml, or a + # profile has just been asserted. but really, you + # should be using the metadata database if you're + # using this core. + self.fam.handle_events_in_interval(0.1) + client = self.pipe.recv() + self.logger.debug("Building configuration for %s" % client) + config = \ + lxml.etree.tostring(self.BuildConfiguration(client)) + self.logger.debug("Returning configuration for %s to main " + "process" % client) + self.pipe.send(config) + self.logger.debug("Returned configuration for %s to main " + "process" % client) + except KeyboardInterrupt: + break + self.shutdown() + + +class Core(BuiltinCore): + """ A multiprocessing core that delegates building the actual + client configurations to + :class:`Bcfg2.Server.MultiprocessingCore.ChildCore` objects. The + parent process doesn't build any children itself; all calls to + :func:`GetConfig` are delegated to children. All other calls are + handled by the parent process. """ + + #: How long to wait for a child process to shut down cleanly + #: before it is terminated. + shutdown_timeout = 10.0 + + def __init__(self, setup): + BuiltinCore.__init__(self, setup) + if setup['children'] is None: + setup['children'] = multiprocessing.cpu_count() + + #: A dict of child name -> one end of the + #: :class:`multiprocessing.Pipe` object used to communicate + #: with that child. (The child is given the other end of the + #: Pipe.) + self.pipes = dict() + + #: A queue that keeps track of which children are available to + #: render a configuration. A child is popped from the queue + #: when it starts to render a config, then it's pushed back on + #: when it's done. This lets us use a blocking call to + #: :func:`Queue.Queue.get` when waiting for an available + #: child. + self.available_children = Queue(maxsize=self.setup['children']) + + # sigh. multiprocessing was added in py2.6, which is when the + # camelCase methods for threading objects were deprecated in + # favor of the Pythonic under_score methods. So + # multiprocessing.Event *only* has is_set(), while + # threading.Event has *both* isSet() and is_set(). In order + # to make the core work with Python 2.4+, and with both + # multiprocessing and threading Event objects, we just + # monkeypatch self.terminate to have isSet(). + self.terminate = DualEvent(threading_event=self.terminate) + + def _run(self): + for cnum in range(self.setup['children']): + name = "Child-%s" % cnum + (mainpipe, childpipe) = multiprocessing.Pipe() + self.pipes[name] = mainpipe + self.logger.debug("Starting child %s" % name) + childcore = ChildCore(self.setup, childpipe, self.terminate) + child = multiprocessing.Process(target=childcore.run, name=name) + child.start() + self.logger.debug("Child %s started with PID %s" % (name, + child.pid)) + self.available_children.put(name) + return BuiltinCore._run(self) + + def shutdown(self): + BuiltinCore.shutdown(self) + for child in multiprocessing.active_children(): + self.logger.debug("Shutting down child %s" % child.name) + child.join(self.shutdown_timeout) + if child.is_alive(): + self.logger.error("Waited %s seconds to shut down %s, " + "terminating" % (self.shutdown_timeout, + child.name)) + child.terminate() + else: + self.logger.debug("Child %s shut down" % child.name) + self.logger.debug("All children shut down") + + @exposed + def GetConfig(self, address): + client = self.resolve_client(address)[0] + childname = self.available_children.get() + self.logger.debug("Building configuration on child %s" % childname) + pipe = self.pipes[childname] + pipe.send(client) + config = pipe.recv() + self.available_children.put_nowait(childname) + return config diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py index 2bc82caa9..507973fa6 100644 --- a/src/lib/Bcfg2/Server/Plugins/Metadata.py +++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py @@ -556,6 +556,12 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, open(os.path.join(repo, cls.name, fname), "w").write(kwargs[aname]) + @property + def use_database(self): + """ Expose self._use_db publicly for use in + :class:`Bcfg2.Server.MultiprocessingCore.ChildCore` """ + return self._use_db + def _handle_file(self, fname): """ set up the necessary magic for handling a metadata file (clients.xml or groups.xml, e.g.) """ diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py index 567a16c40..8c272cf53 100644 --- a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py +++ b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py @@ -512,8 +512,9 @@ class Packages(Bcfg2.Server.Plugin.Plugin, collection = cclass(metadata, relevant, self.cachepath, self.data, debug=self.debug_flag) ckey = collection.cachekey - self.clients[metadata.hostname] = ckey - self.collections[ckey] = collection + if cclass != Collection: + self.clients[metadata.hostname] = ckey + self.collections[ckey] = collection return collection def get_additional_data(self, metadata): diff --git a/src/lib/Bcfg2/Server/SSLServer.py b/src/lib/Bcfg2/Server/SSLServer.py index eea2183f7..8bdcf0500 100644 --- a/src/lib/Bcfg2/Server/SSLServer.py +++ b/src/lib/Bcfg2/Server/SSLServer.py @@ -290,7 +290,10 @@ class XMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): raise except socket.error: err = sys.exc_info()[1] - if err[0] == 32: + if isinstance(err, socket.timeout): + self.logger.warning("Connection timed out for %s" % + self.client_address[0]) + elif err[0] == 32: self.logger.warning("Connection dropped from %s" % self.client_address[0]) elif err[0] == 104: @@ -423,7 +426,9 @@ class XMLRPCServer(SocketServer.ThreadingMixIn, SSLServer, def serve_forever(self): """Serve single requests until (self.serve == False).""" self.serve = True - self.task_thread = threading.Thread(target=self._tasks_thread) + self.task_thread = \ + threading.Thread(name="%sThread" % self.__class__.__name__, + target=self._tasks_thread) self.task_thread.start() self.logger.info("serve_forever() [start]") signal.signal(signal.SIGINT, self._handle_shutdown_signal) diff --git a/src/sbin/bcfg2-server b/src/sbin/bcfg2-server index 33ee327fc..95413d6cf 100755 --- a/src/sbin/bcfg2-server +++ b/src/sbin/bcfg2-server @@ -24,18 +24,29 @@ def main(): print("Could not read %s" % setup['configfile']) sys.exit(1) - if setup['backend'] not in ['best', 'cherrypy', 'builtin']: + # TODO: normalize case of various core modules so we can add a new + # core without modifying this script + backends = dict(cherrypy='CherryPyCore', + builtin='BuiltinCore', + best='BuiltinCore', + multiprocessing='MultiprocessingCore') + + if setup['backend'] not in backends: print("Unknown server backend %s, using 'best'" % setup['backend']) setup['backend'] = 'best' - if setup['backend'] == 'cherrypy': - try: - from Bcfg2.Server.CherryPyCore import Core - except ImportError: - err = sys.exc_info()[1] - print("Unable to import CherryPy server core: %s" % err) - raise - elif setup['backend'] == 'builtin' or setup['backend'] == 'best': - from Bcfg2.Server.BuiltinCore import Core + + coremodule = backends[setup['backend']] + try: + Core = getattr(__import__("Bcfg2.Server.%s" % coremodule).Server, + coremodule).Core + except ImportError: + err = sys.exc_info()[1] + print("Unable to import %s server core: %s" % (setup['backend'], err)) + raise + except AttributeError: + err = sys.exc_info()[1] + print("Unable to load %s server core: %s" % (setup['backend'], err)) + raise try: core = Core() |