summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/development/plugins.txt127
-rw-r--r--src/lib/Bcfg2/Server/Plugin/__init__.py11
-rw-r--r--src/lib/Bcfg2/Server/Plugin/base.py106
-rw-r--r--src/lib/Bcfg2/Server/Plugin/exceptions.py36
-rw-r--r--src/lib/Bcfg2/Server/Plugin/helpers.py (renamed from src/lib/Bcfg2/Server/Plugin.py)689
-rw-r--r--src/lib/Bcfg2/Server/Plugin/interfaces.py548
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py83
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testexceptions.py47
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py (renamed from testsuite/Testsrc/Testlib/TestServer/TestPlugin.py)510
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testinterfaces.py342
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugin/__init__.py17
11 files changed, 1257 insertions, 1259 deletions
diff --git a/doc/development/plugins.txt b/doc/development/plugins.txt
index 2609595a7..bb1f0f046 100644
--- a/doc/development/plugins.txt
+++ b/doc/development/plugins.txt
@@ -48,125 +48,10 @@ With the exceptions of :class:`Bcfg2.Server.Plugin.Statistics` and
listed below do **not** inherit from Plugin; they simply provide
interfaces that a given plugin may or must implement.
-Generator
-^^^^^^^^^
-
-.. autoclass:: Bcfg2.Server.Plugin.Generator
-
-Examples are :ref:`server-plugins-generators-cfg` and
-:ref:`server-plugins-generators-sshbase`.
-
-Structure
-^^^^^^^^^
-
-.. autoclass:: Bcfg2.Server.Plugin.Structure
-
-:ref:`server-plugins-structures-bundler-index` is a Structure plugin.
-
-Metadata
-^^^^^^^^
-
-.. autoclass:: Bcfg2.Server.Plugin.Metadata
-
-:ref:`server-plugins-grouping-metadata` is a Metadata plugin.
-
-Connector
-^^^^^^^^^
-
-.. autoclass:: Bcfg2.Server.Plugin.Connector
-
-Connector plugins include
-:ref:`server-plugins-grouping-grouppatterns`,
-:ref:`server-plugins-connectors-properties`, and
-:ref:`server-plugins-probes-index`.
-
-Probing
-^^^^^^^
-
-.. autoclass:: Bcfg2.Server.Plugin.Probing
-
-Examples include :ref:`server-plugins-probes-index` and
-:ref:`server-plugins-probes-fileprobes`.
-
-Statistics
+Interfaces
^^^^^^^^^^
-.. autoclass:: Bcfg2.Server.Plugin.Statistics
-
-The Statistics object is itself a :class:`Bcfg2.Server.Plugin.Plugin`
-object, so objects that inherit from Statistics do not have to also
-inherit from Plugin.
-
-ThreadedStatistics
-^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: Bcfg2.Server.Plugin.ThreadedStatistics
-
-:ref:`server-plugins-statistics-dbstats` is an example of a
-ThreadedStatistics plugin.
-
-The ThreadedStatistics object is itself a
-:class:`Bcfg2.Server.Plugin.Plugin` object, so objects that inherit
-from ThreadedStatistics do not have to also inherit from Plugin.
-
-PullSource
-^^^^^^^^^^
-
-.. autoclass:: Bcfg2.Server.Plugin.PullSource
-
-:ref:`server-plugins-statistics-dbstats` is an example of a plugin
-that implements the PullSource interface
-
-PullTarget
-^^^^^^^^^^
-
-.. autoclass:: Bcfg2.Server.Plugin.PullTarget
-
-:ref:`server-plugins-generators-sshbase` is an example of a plugin
-that implements the PullTarget interface
-
-Decision
-^^^^^^^^
-
-.. autoclass:: Bcfg2.Server.Plugin.Decision
-
-:ref:`server-plugins-generators-decisions` is an example of a Decision
-plugin, and has much more information about how decisions are used.
-
-StructureValidator
-^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: Bcfg2.Server.Plugin.StructureValidator
-
-Examples are :ref:`server-plugins-structures-defaults` and
-:ref:`server-plugins-structures-deps`.
-
-GoalValidator
-^^^^^^^^^^^^^
-
-.. autoclass:: Bcfg2.Server.Plugin.GoalValidator
-
-An example of a GoalValidator plugin would be the ServiceCompat plugin
-that is used to provide old-style Service tag attributes to older
-clients from a Bcfg2 1.3.0 server. As a final stage of configuration
-generation, it translates the new "restart" and "install" attributes
-into the older "mode" attribute.
-
-Version
-^^^^^^^
-
-.. autoclass:: Bcfg2.Server.Plugin.Version
-
-Examples include :ref:`server-plugins-version-git` and
-:ref:`server-plugins-version-svn2`.
-
-ClientRunHooks
-^^^^^^^^^^^^^^
-
-.. autoclass:: Bcfg2.Server.Plugin.ClientRunHooks
-
-Examples are :ref:`server-plugins-misc-trigger` and
-:ref:`server-plugins-connectors-puppetenc`.
+.. automodule:: Bcfg2.Server.Plugin.interfaces
Exposing XML-RPC Functions
--------------------------
@@ -210,13 +95,9 @@ functions, you could run::
Plugin Helper Classes
---------------------
-.. autoclass:: Bcfg2.Server.Plugin.Debuggable
+.. automodule:: Bcfg2.Server.Plugin.helpers
Plugin Exceptions
-----------------
-.. autoexception:: Bcfg2.Server.Plugin.ValidationError
-.. autoexception:: Bcfg2.Server.Plugin.PluginInitError
-.. autoexception:: Bcfg2.Server.Plugin.PluginExecutionError
-.. autoexception:: Bcfg2.Server.Plugin.MetadataConsistencyError
-.. autoexception:: Bcfg2.Server.Plugin.MetadataRuntimeError
+.. automodule:: Bcfg2.Server.Plugin.exceptions
diff --git a/src/lib/Bcfg2/Server/Plugin/__init__.py b/src/lib/Bcfg2/Server/Plugin/__init__.py
new file mode 100644
index 000000000..487a457e6
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugin/__init__.py
@@ -0,0 +1,11 @@
+""" Bcfg2 server plugin base classes, interfaces, and helper
+objects. """
+
+import os
+import sys
+sys.path.append(os.path.dirname(__file__))
+
+from base import *
+from interfaces import *
+from helpers import *
+from exceptions import *
diff --git a/src/lib/Bcfg2/Server/Plugin/base.py b/src/lib/Bcfg2/Server/Plugin/base.py
new file mode 100644
index 000000000..98427e726
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugin/base.py
@@ -0,0 +1,106 @@
+"""This module provides the base class for Bcfg2 server plugins."""
+
+import os
+import logging
+
+class Debuggable(object):
+ """ Mixin to add a debugging interface to an object and expose it
+ via XML-RPC on :class:`Bcfg2.Server.Plugin.Plugin` objects """
+
+ #: List of names of methods to be exposed as XML-RPC functions
+ __rmi__ = ['toggle_debug']
+
+ def __init__(self, name=None):
+ if name is None:
+ name = "%s.%s" % (self.__class__.__module__,
+ self.__class__.__name__)
+ self.debug_flag = False
+ self.logger = logging.getLogger(name)
+
+ def toggle_debug(self):
+ """ Turn debugging output on or off.
+
+ :returns: bool - The new value of the debug flag
+ """
+ self.debug_flag = not self.debug_flag
+ self.debug_log("%s: debug_flag = %s" % (self.__class__.__name__,
+ self.debug_flag),
+ flag=True)
+ return self.debug_flag
+
+ def debug_log(self, message, flag=None):
+ """ Log a message at the debug level.
+
+ :param message: The message to log
+ :type message: string
+ :param flag: Override the current debug flag with this value
+ :type flag: bool
+ :returns: None
+ """
+ if (flag is None and self.debug_flag) or flag:
+ self.logger.error(message)
+
+
+class Plugin(Debuggable):
+ """ The base class for all Bcfg2 Server plugins. """
+
+ #: The name of the plugin.
+ name = 'Plugin'
+
+ #: The email address of the plugin author.
+ __author__ = 'bcfg-dev@mcs.anl.gov'
+
+ #: Plugin is experimental. Use of this plugin will produce a log
+ #: message alerting the administrator that an experimental plugin
+ #: is in use.
+ experimental = False
+
+ #: Plugin is deprecated and will be removed in a future release.
+ #: Use of this plugin will produce a log message alerting the
+ #: administrator that an experimental plugin is in use.
+ deprecated = False
+
+ #: Plugin conflicts with the list of other plugin names
+ conflicts = []
+
+ #: Plugins of the same type are processed in order of ascending
+ #: sort_order value. Plugins with the same sort_order are sorted
+ #: alphabetically by their name.
+ sort_order = 500
+
+ def __init__(self, core, datastore):
+ """ Initialize the plugin.
+
+ :param core: The Bcfg2.Server.Core initializing the plugin
+ :type core: Bcfg2.Server.Core
+ :param datastore: The path to the Bcfg2 repository on the
+ filesystem
+ :type datastore: string
+ :raises: Bcfg2.Server.Plugin.PluginInitError
+ """
+ object.__init__(self)
+ self.Entries = {}
+ self.core = core
+ self.data = os.path.join(datastore, self.name)
+ self.running = True
+ Debuggable.__init__(self, name=self.name)
+
+ @classmethod
+ def init_repo(cls, repo):
+ """ Perform any tasks necessary to create an initial Bcfg2
+ repository.
+
+ :param repo: The path to the Bcfg2 repository on the filesystem
+ :type repo: string
+ :returns: None
+ """
+ os.makedirs(os.path.join(repo, cls.name))
+
+ def shutdown(self):
+ """ Perform shutdown tasks for the plugin
+
+ :returns: None """
+ self.running = False
+
+ def __str__(self):
+ return "%s Plugin" % self.__class__.__name__
diff --git a/src/lib/Bcfg2/Server/Plugin/exceptions.py b/src/lib/Bcfg2/Server/Plugin/exceptions.py
new file mode 100644
index 000000000..bc8c62acd
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugin/exceptions.py
@@ -0,0 +1,36 @@
+""" Exceptions for Bcfg2 Server Plugins."""
+
+class PluginInitError(Exception):
+ """Error raised in cases of :class:`Bcfg2.Server.Plugin.Plugin`
+ initialization errors."""
+ pass
+
+
+class PluginExecutionError(Exception):
+ """Error raised in case of :class:`Bcfg2.Server.Plugin.Plugin`
+ execution errors."""
+ pass
+
+
+class MetadataConsistencyError(Exception):
+ """This error gets raised when metadata is internally inconsistent."""
+ pass
+
+
+class MetadataRuntimeError(Exception):
+ """This error is raised when the metadata engine is called prior
+ to reading enough data, or for other
+ :class:`Bcfg2.Server.Plugin.Metadata` errors. """
+ pass
+
+
+class ValidationError(Exception):
+ """ Exception raised by
+ :class:`Bcfg2.Server.Plugin.StructureValidator` and
+ :class:`Bcfg2.Server.Plugin.GoalValidator` objects """
+
+
+class SpecificityError(Exception):
+ """ Thrown by :class:`Bcfg2.Server.Plugin.Specificity` in case of
+ filename parse failure."""
+ pass
diff --git a/src/lib/Bcfg2/Server/Plugin.py b/src/lib/Bcfg2/Server/Plugin/helpers.py
index 0b2f7cee0..74cf4b3c4 100644
--- a/src/lib/Bcfg2/Server/Plugin.py
+++ b/src/lib/Bcfg2/Server/Plugin/helpers.py
@@ -1,4 +1,4 @@
-"""This module provides the baseclass for Bcfg2 Server Plugins."""
+""" Helper classes for Bcfg2 server plugins """
import os
import re
@@ -6,12 +6,13 @@ import sys
import copy
import logging
import operator
-import threading
import lxml.etree
import Bcfg2.Server
import Bcfg2.Options
-from Bcfg2.Compat import ConfigParser, CmpMixin, reduce, Queue, Empty, \
- Full, cPickle
+from Bcfg2.Compat import CmpMixin
+from base import *
+from interfaces import *
+from exceptions import *
try:
import django
@@ -31,7 +32,7 @@ default_file_metadata = Bcfg2.Options.OptionParser(opts)
default_file_metadata.parse([])
del default_file_metadata['args']
-logger = logging.getLogger('Bcfg2.Server.Plugin')
+logger = logging.getLogger(__name__)
info_regex = re.compile('owner:(\s)*(?P<owner>\S+)|' +
'group:(\s)*(?P<group>\S+)|' +
@@ -57,133 +58,6 @@ def bind_info(entry, metadata, infoxml=None, default=default_file_metadata):
entry.set(attr, val)
-class PluginInitError(Exception):
- """Error raised in cases of :class:`Bcfg2.Server.Plugin.Plugin`
- initialization errors."""
- pass
-
-
-class PluginExecutionError(Exception):
- """Error raised in case of :class:`Bcfg2.Server.Plugin.Plugin`
- execution errors."""
- pass
-
-
-class MetadataConsistencyError(Exception):
- """This error gets raised when metadata is internally inconsistent."""
- pass
-
-
-class MetadataRuntimeError(Exception):
- """This error is raised when the metadata engine is called prior
- to reading enough data, or for other
- :class:`Bcfg2.Server.Plugin.Metadata` errors. """
- pass
-
-
-class Debuggable(object):
- """ Mixin to add a debugging interface to an object and expose it
- via XML-RPC on :class:`Bcfg2.Server.Plugin.Plugin` objects """
-
- #: List of names of methods to be exposed as XML-RPC functions
- __rmi__ = ['toggle_debug']
-
- def __init__(self, name=None):
- if name is None:
- name = "%s.%s" % (self.__class__.__module__,
- self.__class__.__name__)
- self.debug_flag = False
- self.logger = logging.getLogger(name)
-
- def toggle_debug(self):
- """ Turn debugging output on or off.
-
- :returns: bool - The new value of the debug flag
- """
- self.debug_flag = not self.debug_flag
- self.debug_log("%s: debug_flag = %s" % (self.__class__.__name__,
- self.debug_flag),
- flag=True)
- return self.debug_flag
-
- def debug_log(self, message, flag=None):
- """ Log a message at the debug level.
-
- :param message: The message to log
- :type message: string
- :param flag: Override the current debug flag with this value
- :type flag: bool
- :returns: None
- """
- if (flag is None and self.debug_flag) or flag:
- self.logger.error(message)
-
-
-class Plugin(Debuggable):
- """ The base class for all Bcfg2 Server plugins. """
-
- #: The name of the plugin.
- name = 'Plugin'
-
- #: The email address of the plugin author.
- __author__ = 'bcfg-dev@mcs.anl.gov'
-
- #: Plugin is experimental. Use of this plugin will produce a log
- #: message alerting the administrator that an experimental plugin
- #: is in use.
- experimental = False
-
- #: Plugin is deprecated and will be removed in a future release.
- #: Use of this plugin will produce a log message alerting the
- #: administrator that an experimental plugin is in use.
- deprecated = False
-
- #: Plugin conflicts with the list of other plugin names
- conflicts = []
-
- #: Plugins of the same type are processed in order of ascending
- #: sort_order value. Plugins with the same sort_order are sorted
- #: alphabetically by their name.
- sort_order = 500
-
- def __init__(self, core, datastore):
- """ Initialize the plugin.
-
- :param core: The Bcfg2.Server.Core initializing the plugin
- :type core: Bcfg2.Server.Core
- :param datastore: The path to the Bcfg2 repository on the
- filesystem
- :type datastore: string
- :raises: Bcfg2.Server.Plugin.PluginInitError
- """
- object.__init__(self)
- self.Entries = {}
- self.core = core
- self.data = os.path.join(datastore, self.name)
- self.running = True
- Debuggable.__init__(self, name=self.name)
-
- @classmethod
- def init_repo(cls, repo):
- """ Perform any tasks necessary to create an initial Bcfg2
- repository.
-
- :param repo: The path to the Bcfg2 repository on the filesystem
- :type repo: string
- :returns: None
- """
- os.makedirs(os.path.join(repo, cls.name))
-
- def shutdown(self):
- """ Perform shutdown tasks for the plugin
-
- :returns: None """
- self.running = False
-
- def __str__(self):
- return "%s Plugin" % self.__class__.__name__
-
-
class DatabaseBacked(Plugin):
@property
def _use_db(self):
@@ -204,551 +78,6 @@ class PluginDatabaseModel(object):
app_label = "Server"
-class Generator(object):
- """ Generator plugins contribute to literal client configurations.
- That is, they generate entry contents.
-
- An entry is generated in one of two ways:
-
- #. The Bcfg2 core looks in the ``Entries`` dict attribute of the
- plugin object. ``Entries`` is expected to be a dict whose keys
- are entry tags (e.g., ``"Path"``, ``"Service"``, etc.) and
- whose values are dicts; those dicts should map the ``name``
- attribute of an entry to a callable that will be called to
- generate the content. The callable will receive two arguments:
- the abstract entry (as an lxml.etree._Element object), and the
- client metadata object the entry is being generated for.
-
- #. If the entry is not listed in ``Entries``, the Bcfg2 core calls
- :func:`Bcfg2.Server.Plugin.Generator.HandlesEntry`; if that
- returns True, then it calls
- :func:`Bcfg2.Server.Plugin.Generator.HandleEntry`.
- """
-
- def HandlesEntry(self, entry, metadata):
- """ HandlesEntry is the slow path method for routing
- configuration binding requests. It is called if the
- ``Entries`` dict does not contain a method for binding the
- entry.
-
- :param entry: The entry to bind
- :type entry: lxml.etree._Element
- :param metadata: The client metadata
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :return: bool - Whether or not this plugin can handle the entry
- :raises: Bcfg2.Server.Plugin.PluginExecutionError
- """
- return False
-
- def HandleEntry(self, entry, metadata):
- """ HandlesEntry is the slow path method for binding
- configuration binding requests. It is called if the
- ``Entries`` dict does not contain a method for binding the
- entry, and :func:`Bcfg2.Server.Plugin.Generator.HandlesEntry`
- returns True.
-
- :param entry: The entry to bind
- :type entry: lxml.etree._Element
- :param metadata: The client metadata
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :return: lxml.etree._Element - The fully bound entry
- :raises: Bcfg2.Server.Plugin.PluginExecutionError
- """
- return entry
-
-
-class Structure(object):
- """ Structure Plugins contribute to abstract client
- configurations. That is, they produce lists of entries that will
- be generated for a client. """
-
- def BuildStructures(self, metadata):
- """ Build a list of lxml.etree._Element objects that will be
- added to the top-level ``<Configuration>`` tag of the client
- configuration. Consequently, each object in the list returned
- by ``BuildStructures()`` must consist of a container tag
- (e.g., ``<Bundle>`` or ``<Independent>``) which contains the
- entry tags. It must not return a list of entry tags.
-
- :param metadata: The client metadata
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :return: list of lxml.etree._Element objects
- """
- raise NotImplementedError
-
-
-class Metadata(object):
- """Signal metadata capabilities for this plugin"""
- def viz(self, hosts, bundles, key, only_client, colors):
- """ Return a string containing a graphviz document that maps
- out the Metadata for :ref:`bcfg2-admin viz <server-admin-viz>`
-
- :param hosts: Include hosts in the graph
- :type hosts: bool
- :param bundles: Include bundles in the graph
- :type bundles: bool
- :param key: Include a key in the graph
- :type key: bool
- :param only_client: Only include data for the specified client
- :type only_client: string
- :param colors: Use the specified graphviz colors
- :type colors: list of strings
- :return: string
- """
- return ''
-
- def set_version(self, client, version):
- """ Set the version for the named client to the specified
- version string.
-
- :param client: Hostname of the client
- :type client: string
- :param profile: Client Bcfg2 version
- :type profile: string
- :return: None
- :raises: Bcfg2.Server.Plugin.MetadataRuntimeError,
- Bcfg2.Server.Plugin.MetadataConsistencyError
- """
- pass
-
- def set_profile(self, client, profile, address):
- """ Set the profile for the named client to the named profile
- group.
-
- :param client: Hostname of the client
- :type client: string
- :param profile: Name of the profile group
- :type profile: string
- :param address: Address pair of ``(<ip address>, <hostname>)``
- :type address: tuple
- :return: None
- :raises: Bcfg2.Server.Plugin.MetadataRuntimeError,
- Bcfg2.Server.Plugin.MetadataConsistencyError
- """
- pass
-
- def resolve_client(self, address, cleanup_cache=False):
- """ Resolve the canonical name of this client. If this method
- is not implemented, the hostname claimed by the client is
- used. (This may be a security risk; it's highly recommended
- that you implement ``resolve_client`` if you are writing a
- Metadata plugin.)
-
- :param address: Address pair of ``(<ip address>, <hostname>)``
- :type address: tuple
- :param cleanup_cache: Whether or not to remove expire the
- entire client hostname resolution class
- :type cleanup_cache: bool
- :return: string - canonical client hostname
- :raises: Bcfg2.Server.Plugin.MetadataRuntimeError,
- Bcfg2.Server.Plugin.MetadataConsistencyError
- """
- return address[1]
-
- def AuthenticateConnection(self, cert, user, password, address):
- """ Authenticate the given client.
-
- :param cert: an x509 certificate
- :type cert: dict
- :param user: The username of the user trying to authenticate
- :type user: string
- :param password: The password supplied by the client
- :type password: string
- :param addresspair: An address pair of ``(<ip address>,
- <hostname>)``
- :type addresspair: tuple
- :return: bool - True if the authenticate succeeds, False otherwise
- """
- raise NotImplementedError
-
- def get_initial_metadata(self, client_name):
- """ Return a
- :class:`Bcfg2.Server.Plugins.Metadata.ClientMetadata` object
- that fully describes everything the Metadata plugin knows
- about the named client.
-
- :param client_name: The hostname of the client
- :type client_name: string
- :return: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- """
- raise NotImplementedError
-
- def merge_additional_data(self, imd, source, data):
- """ Add arbitrary data from a
- :class:`Bcfg2.Server.Plugin.Connector` plugin to the given
- metadata object.
-
- :param imd: An initial metadata object
- :type imd: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :param source: The name of the plugin providing this data
- :type source: string
- :param data: The data to add
- :type data: any
- :return: None
- """
- raise NotImplementedError
-
- def merge_additional_groups(self, imd, groups):
- """ Add groups from a
- :class:`Bcfg2.Server.Plugin.Connector` plugin to the given
- metadata object.
-
- :param imd: An initial metadata object
- :type imd: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :param groups: The groups to add
- :type groups: list of strings
- :return: None
- """
- raise NotImplementedError
-
-
-class Connector(object):
- """ Connector plugins augment client metadata instances with
- additional data, additional groups, or both. """
-
- def get_additional_groups(self, metadata):
- """ Return a list of additional groups for the given client.
-
- :param metadata: The client metadata
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :return: list of strings
- """
- return list()
-
- def get_additional_data(self, metadata):
- """ Return arbitrary additional data for the given
- ClientMetadata object. By convention this is usually a dict
- object, but doesn't need to be.
-
- :param metadata: The client metadata
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :return: list of strings
- """
- return dict()
-
-
-class Probing(object):
- """ Probing plugins can collect data from clients and process it.
- """
-
- def GetProbes(self, metadata):
- """ Return a list of probes for the given client. Each probe
- should be an lxml.etree._Element object that adheres to
- the following specification. Each probe must the following
- attributes:
-
- * ``name``: The unique name of the probe.
- * ``source``: The origin of the probe; probably the name of
- the plugin that supplies the probe.
- * ``interpreter``: The command that will be run on the client
- to interpret the probe script. Compiled (i.e.,
- non-interpreted) probes are not supported.
-
- The text of the XML tag should be the contents of the probe,
- i.e., the code that will be run on the client.
-
- :param metadata: The client metadata
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :return: list of lxml.etree._Element objects
- """
- raise NotImplementedError
-
- def ReceiveData(self, metadata, datalist):
- """ Process data returned from the probes for the given
- client. ``datalist`` is a list of lxml.etree._Element
- objects, each of which is a single tag; the ``name`` attribute
- holds the unique name of the probe that was run, and the text
- contents of the tag hold the results of the probe.
-
- :param metadata: The client metadata
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :param datalist: The probe data
- :type datalist: list of lxml.etree._Element objects
- :return: None
- """
- raise NotImplementedError
-
-
-class Statistics(Plugin):
- """ Statistics plugins handle statistics for clients. In general,
- you should avoid using Statistics and use
- :class:`Bcfg2.Server.Plugin.ThreadedStatistics` instead."""
-
- def process_statistics(self, client, xdata):
- """ Process the given XML statistics data for the specified
- client.
-
- :param metadata: The client metadata
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :param data: The statistics data
- :type data: lxml.etree._Element
- :return: None
- """
- raise NotImplementedError
-
-
-class ThreadedStatistics(Statistics, threading.Thread):
- """ ThreadedStatistics plugins process client statistics in a
- separate thread. """
-
- def __init__(self, core, datastore):
- Statistics.__init__(self, core, datastore)
- threading.Thread.__init__(self)
- # Event from the core signaling an exit
- self.terminate = core.terminate
- self.work_queue = Queue(100000)
- self.pending_file = os.path.join(datastore, "etc",
- "%s.pending" % self.name)
- self.daemon = False
- self.start()
-
- def _save(self):
- """Save any pending data to a file."""
- pending_data = []
- try:
- while not self.work_queue.empty():
- (metadata, data) = self.work_queue.get_nowait()
- try:
- pending_data.append((metadata.hostname,
- lxml.etree.tostring(data,
- xml_declaration=False).decode("UTF-8")))
- except:
- err = sys.exc_info()[1]
- self.logger.warning("Dropping interaction for %s: %s" %
- (metadata.hostname, err))
- except Empty:
- pass
-
- try:
- savefile = open(self.pending_file, 'w')
- cPickle.dump(pending_data, savefile)
- savefile.close()
- self.logger.info("Saved pending %s data" % self.name)
- except:
- err = sys.exc_info()[1]
- self.logger.warning("Failed to save pending data: %s" % err)
-
- def _load(self):
- """Load any pending data from a file."""
- if not os.path.exists(self.pending_file):
- return True
- pending_data = []
- try:
- savefile = open(self.pending_file, 'r')
- pending_data = cPickle.load(savefile)
- savefile.close()
- except Exception:
- e = sys.exc_info()[1]
- self.logger.warning("Failed to load pending data: %s" % e)
- return False
- for (pmetadata, pdata) in pending_data:
- # check that shutdown wasnt called early
- if self.terminate.isSet():
- return False
-
- try:
- while True:
- try:
- metadata = self.core.build_metadata(pmetadata)
- break
- except MetadataRuntimeError:
- pass
-
- self.terminate.wait(5)
- if self.terminate.isSet():
- return False
-
- self.work_queue.put_nowait((metadata,
- lxml.etree.XML(pdata,
- parser=Bcfg2.Server.XMLParser)))
- except Full:
- self.logger.warning("Queue.Full: Failed to load queue data")
- break
- except lxml.etree.LxmlError:
- lxml_error = sys.exc_info()[1]
- self.logger.error("Unable to load saved interaction: %s" %
- lxml_error)
- except MetadataConsistencyError:
- self.logger.error("Unable to load metadata for save "
- "interaction: %s" % pmetadata)
- try:
- os.unlink(self.pending_file)
- except:
- self.logger.error("Failed to unlink save file: %s" %
- self.pending_file)
- self.logger.info("Loaded pending %s data" % self.name)
- return True
-
- def run(self):
- if not self._load():
- return
- while not self.terminate.isSet() and self.work_queue != None:
- try:
- (client, xdata) = self.work_queue.get(block=True, timeout=2)
- except Empty:
- continue
- except Exception:
- e = sys.exc_info()[1]
- self.logger.error("ThreadedStatistics: %s" % e)
- continue
- self.handle_statistic(client, xdata)
- if self.work_queue != None and not self.work_queue.empty():
- self._save()
-
- def process_statistics(self, metadata, data):
- try:
- self.work_queue.put_nowait((metadata, copy.copy(data)))
- except Full:
- self.logger.warning("%s: Queue is full. Dropping interactions." %
- self.name)
-
- def handle_statistic(self, metadata, data):
- """ Process the given XML statistics data for the specified
- client object. This differs from the
- :func:`Bcfg2.Server.Plugin.Statistics.process_statistics`
- method only in that ThreadedStatistics first adds the data to
- a queue, and then processes them in a separate thread.
-
- :param metadata: The client metadata
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :param data: The statistics data
- :type data: lxml.etree._Element
- :return: None
- """
- raise NotImplementedError
-
-
-class PullSource(object):
- def GetExtra(self, client):
- return []
-
- def GetCurrentEntry(self, client, e_type, e_name):
- raise NotImplementedError
-
-
-class PullTarget(object):
- def AcceptChoices(self, entry, metadata):
- raise NotImplementedError
-
- def AcceptPullData(self, specific, new_entry, verbose):
- raise NotImplementedError
-
-
-class Decision(object):
- """ Decision plugins produce decision lists for affecting which
- entries are actually installed on clients. """
-
- def GetDecisions(self, metadata, mode):
- """ Return a list of tuples of ``(<entry type>, <entry
- name>)`` to be used as the decision list for the given
- client in the specified mode.
-
- :param metadata: The client metadata
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :param mode: The decision mode ("whitelist" or "blacklist")
- :type mode: string
- :return: list of tuples
- """
- raise NotImplementedError
-
-
-class ValidationError(Exception):
- """ Exception raised by
- :class:`Bcfg2.Server.Plugin.StructureValidator` and
- :class:`Bcfg2.Server.Plugin.GoalValidator` objects """
-
-
-class StructureValidator(object):
- """ StructureValidator plugins can modify the list of structures
- after it has been created but before the entries have been
- concretely bound. """
-
- def validate_structures(self, metadata, structures):
- """ Given a list of structures (i.e., of tags that contain
- entry tags), modify that list or the structures in it
- in-place.
-
- :param metadata: The client metadata
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :param config: A list of lxml.etree._Element objects
- describing the structures for this client
- :type config: list
- :returns: None
- :raises: Bcfg2.Server.Plugin.ValidationError
- """
- raise NotImplementedError
-
-
-class GoalValidator(object):
- """ GoalValidator plugins can modify the concretely-bound configuration of
- a client as a last stage before the configuration is sent to the
- client. """
-
- def validate_goals(self, metadata, config):
- """ Given a monolithic XML document of the full configuration,
- modify the document in-place.
-
- :param metadata: The client metadata
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :param config: The full configuration for the client
- :type config: lxml.etree._Element
- :returns: None
- :raises: Bcfg2.Server.Plugin.ValidationError
- """
- raise NotImplementedError
-
-
-class Version(object):
- """ Version plugins interact with various version control systems. """
-
- def get_revision(self):
- """ Return the current revision of the Bcfg2 specification.
- This will be included in the ``revision`` attribute of the
- top-level tag of the XML configuration sent to the client.
-
- :returns: string - the current version
- """
- raise NotImplementedError
-
-
-class ClientRunHooks(object):
- """ ClientRunHooks can hook into various parts of a client run to
- perform actions at various times without needing to pretend to be
- a different plugin type. """
-
- def start_client_run(self, metadata):
- """ Invoked at the start of a client run, after all probe data
- has been received and decision lists have been queried (if
- applicable), but before the configuration is generated.
-
- :param metadata: The client metadata object
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :returns: None
- """
- pass
-
- def end_client_run(self, metadata):
- """ Invoked at the end of a client run, immediately after
- :class:`Bcfg2.Server.Plugin.GoalValidator` plugins have been run
- and just before the configuration is returned to the client.
-
- :param metadata: The client metadata object
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :returns: None
- """
- pass
-
- def end_statistics(self, metadata):
- """ Invoked after statistics are processed for a client.
-
- :param metadata: The client metadata object
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :returns: None
- """
- pass
-
-# the rest of the file contains classes for coherent file caching
-
class FileBacked(object):
"""This object caches file data in memory.
HandleEvent is called whenever fam registers an event.
@@ -1321,12 +650,6 @@ class PrioDir(Plugin, Generator, XMLDirectoryBacked):
if not key.startswith('__')])
-# new unified EntrySet backend
-class SpecificityError(Exception):
- """Thrown in case of filename parse failure."""
- pass
-
-
class Specificity(CmpMixin):
def __init__(self, all=False, group=False, hostname=False, prio=0,
delta=False):
diff --git a/src/lib/Bcfg2/Server/Plugin/interfaces.py b/src/lib/Bcfg2/Server/Plugin/interfaces.py
new file mode 100644
index 000000000..a6543e9b9
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugin/interfaces.py
@@ -0,0 +1,548 @@
+""" Interface definitions for Bcfg2 server plugins """
+
+import os
+import sys
+import copy
+import threading
+import lxml.etree
+import Bcfg2.Server
+from Bcfg2.Compat import Queue, Empty, Full, cPickle
+from exceptions import *
+from base import Plugin
+
+class Generator(object):
+ """ Generator plugins contribute to literal client configurations.
+ That is, they generate entry contents.
+
+ An entry is generated in one of two ways:
+
+ #. The Bcfg2 core looks in the ``Entries`` dict attribute of the
+ plugin object. ``Entries`` is expected to be a dict whose keys
+ are entry tags (e.g., ``"Path"``, ``"Service"``, etc.) and
+ whose values are dicts; those dicts should map the ``name``
+ attribute of an entry to a callable that will be called to
+ generate the content. The callable will receive two arguments:
+ the abstract entry (as an lxml.etree._Element object), and the
+ client metadata object the entry is being generated for.
+
+ #. If the entry is not listed in ``Entries``, the Bcfg2 core calls
+ :func:`Bcfg2.Server.Plugin.Generator.HandlesEntry`; if that
+ returns True, then it calls
+ :func:`Bcfg2.Server.Plugin.Generator.HandleEntry`.
+ """
+
+ def HandlesEntry(self, entry, metadata):
+ """ HandlesEntry is the slow path method for routing
+ configuration binding requests. It is called if the
+ ``Entries`` dict does not contain a method for binding the
+ entry.
+
+ :param entry: The entry to bind
+ :type entry: lxml.etree._Element
+ :param metadata: The client metadata
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :return: bool - Whether or not this plugin can handle the entry
+ :raises: Bcfg2.Server.Plugin.PluginExecutionError
+ """
+ return False
+
+ def HandleEntry(self, entry, metadata):
+ """ HandlesEntry is the slow path method for binding
+ configuration binding requests. It is called if the
+ ``Entries`` dict does not contain a method for binding the
+ entry, and :func:`Bcfg2.Server.Plugin.Generator.HandlesEntry`
+ returns True.
+
+ :param entry: The entry to bind
+ :type entry: lxml.etree._Element
+ :param metadata: The client metadata
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :return: lxml.etree._Element - The fully bound entry
+ :raises: Bcfg2.Server.Plugin.PluginExecutionError
+ """
+ return entry
+
+
+class Structure(object):
+ """ Structure Plugins contribute to abstract client
+ configurations. That is, they produce lists of entries that will
+ be generated for a client. """
+
+ def BuildStructures(self, metadata):
+ """ Build a list of lxml.etree._Element objects that will be
+ added to the top-level ``<Configuration>`` tag of the client
+ configuration. Consequently, each object in the list returned
+ by ``BuildStructures()`` must consist of a container tag
+ (e.g., ``<Bundle>`` or ``<Independent>``) which contains the
+ entry tags. It must not return a list of entry tags.
+
+ :param metadata: The client metadata
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :return: list of lxml.etree._Element objects
+ """
+ raise NotImplementedError
+
+
+class Metadata(object):
+ """Signal metadata capabilities for this plugin"""
+ def viz(self, hosts, bundles, key, only_client, colors):
+ """ Return a string containing a graphviz document that maps
+ out the Metadata for :ref:`bcfg2-admin viz <server-admin-viz>`
+
+ :param hosts: Include hosts in the graph
+ :type hosts: bool
+ :param bundles: Include bundles in the graph
+ :type bundles: bool
+ :param key: Include a key in the graph
+ :type key: bool
+ :param only_client: Only include data for the specified client
+ :type only_client: string
+ :param colors: Use the specified graphviz colors
+ :type colors: list of strings
+ :return: string
+ """
+ return ''
+
+ def set_version(self, client, version):
+ """ Set the version for the named client to the specified
+ version string.
+
+ :param client: Hostname of the client
+ :type client: string
+ :param profile: Client Bcfg2 version
+ :type profile: string
+ :return: None
+ :raises: Bcfg2.Server.Plugin.MetadataRuntimeError,
+ Bcfg2.Server.Plugin.MetadataConsistencyError
+ """
+ pass
+
+ def set_profile(self, client, profile, address):
+ """ Set the profile for the named client to the named profile
+ group.
+
+ :param client: Hostname of the client
+ :type client: string
+ :param profile: Name of the profile group
+ :type profile: string
+ :param address: Address pair of ``(<ip address>, <hostname>)``
+ :type address: tuple
+ :return: None
+ :raises: Bcfg2.Server.Plugin.MetadataRuntimeError,
+ Bcfg2.Server.Plugin.MetadataConsistencyError
+ """
+ pass
+
+ def resolve_client(self, address, cleanup_cache=False):
+ """ Resolve the canonical name of this client. If this method
+ is not implemented, the hostname claimed by the client is
+ used. (This may be a security risk; it's highly recommended
+ that you implement ``resolve_client`` if you are writing a
+ Metadata plugin.)
+
+ :param address: Address pair of ``(<ip address>, <hostname>)``
+ :type address: tuple
+ :param cleanup_cache: Whether or not to remove expire the
+ entire client hostname resolution class
+ :type cleanup_cache: bool
+ :return: string - canonical client hostname
+ :raises: Bcfg2.Server.Plugin.MetadataRuntimeError,
+ Bcfg2.Server.Plugin.MetadataConsistencyError
+ """
+ return address[1]
+
+ def AuthenticateConnection(self, cert, user, password, address):
+ """ Authenticate the given client.
+
+ :param cert: an x509 certificate
+ :type cert: dict
+ :param user: The username of the user trying to authenticate
+ :type user: string
+ :param password: The password supplied by the client
+ :type password: string
+ :param addresspair: An address pair of ``(<ip address>,
+ <hostname>)``
+ :type addresspair: tuple
+ :return: bool - True if the authenticate succeeds, False otherwise
+ """
+ raise NotImplementedError
+
+ def get_initial_metadata(self, client_name):
+ """ Return a
+ :class:`Bcfg2.Server.Plugins.Metadata.ClientMetadata` object
+ that fully describes everything the Metadata plugin knows
+ about the named client.
+
+ :param client_name: The hostname of the client
+ :type client_name: string
+ :return: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ """
+ raise NotImplementedError
+
+ def merge_additional_data(self, imd, source, data):
+ """ Add arbitrary data from a
+ :class:`Bcfg2.Server.Plugin.Connector` plugin to the given
+ metadata object.
+
+ :param imd: An initial metadata object
+ :type imd: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :param source: The name of the plugin providing this data
+ :type source: string
+ :param data: The data to add
+ :type data: any
+ :return: None
+ """
+ raise NotImplementedError
+
+ def merge_additional_groups(self, imd, groups):
+ """ Add groups from a
+ :class:`Bcfg2.Server.Plugin.Connector` plugin to the given
+ metadata object.
+
+ :param imd: An initial metadata object
+ :type imd: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :param groups: The groups to add
+ :type groups: list of strings
+ :return: None
+ """
+ raise NotImplementedError
+
+
+class Connector(object):
+ """ Connector plugins augment client metadata instances with
+ additional data, additional groups, or both. """
+
+ def get_additional_groups(self, metadata):
+ """ Return a list of additional groups for the given client.
+
+ :param metadata: The client metadata
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :return: list of strings
+ """
+ return list()
+
+ def get_additional_data(self, metadata):
+ """ Return arbitrary additional data for the given
+ ClientMetadata object. By convention this is usually a dict
+ object, but doesn't need to be.
+
+ :param metadata: The client metadata
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :return: list of strings
+ """
+ return dict()
+
+
+class Probing(object):
+ """ Probing plugins can collect data from clients and process it.
+ """
+
+ def GetProbes(self, metadata):
+ """ Return a list of probes for the given client. Each probe
+ should be an lxml.etree._Element object that adheres to
+ the following specification. Each probe must the following
+ attributes:
+
+ * ``name``: The unique name of the probe.
+ * ``source``: The origin of the probe; probably the name of
+ the plugin that supplies the probe.
+ * ``interpreter``: The command that will be run on the client
+ to interpret the probe script. Compiled (i.e.,
+ non-interpreted) probes are not supported.
+
+ The text of the XML tag should be the contents of the probe,
+ i.e., the code that will be run on the client.
+
+ :param metadata: The client metadata
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :return: list of lxml.etree._Element objects
+ """
+ raise NotImplementedError
+
+ def ReceiveData(self, metadata, datalist):
+ """ Process data returned from the probes for the given
+ client. ``datalist`` is a list of lxml.etree._Element
+ objects, each of which is a single tag; the ``name`` attribute
+ holds the unique name of the probe that was run, and the text
+ contents of the tag hold the results of the probe.
+
+ :param metadata: The client metadata
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :param datalist: The probe data
+ :type datalist: list of lxml.etree._Element objects
+ :return: None
+ """
+ raise NotImplementedError
+
+
+class Statistics(Plugin):
+ """ Statistics plugins handle statistics for clients. In general,
+ you should avoid using Statistics and use
+ :class:`Bcfg2.Server.Plugin.ThreadedStatistics` instead."""
+
+ def process_statistics(self, client, xdata):
+ """ Process the given XML statistics data for the specified
+ client.
+
+ :param metadata: The client metadata
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :param data: The statistics data
+ :type data: lxml.etree._Element
+ :return: None
+ """
+ raise NotImplementedError
+
+
+class ThreadedStatistics(Statistics, threading.Thread):
+ """ ThreadedStatistics plugins process client statistics in a
+ separate thread. """
+
+ def __init__(self, core, datastore):
+ Statistics.__init__(self, core, datastore)
+ threading.Thread.__init__(self)
+ # Event from the core signaling an exit
+ self.terminate = core.terminate
+ self.work_queue = Queue(100000)
+ self.pending_file = os.path.join(datastore, "etc",
+ "%s.pending" % self.name)
+ self.daemon = False
+ self.start()
+
+ def _save(self):
+ """Save any pending data to a file."""
+ pending_data = []
+ try:
+ while not self.work_queue.empty():
+ (metadata, data) = self.work_queue.get_nowait()
+ try:
+ pending_data.append((metadata.hostname,
+ lxml.etree.tostring(data,
+ xml_declaration=False).decode("UTF-8")))
+ except:
+ err = sys.exc_info()[1]
+ self.logger.warning("Dropping interaction for %s: %s" %
+ (metadata.hostname, err))
+ except Empty:
+ pass
+
+ try:
+ savefile = open(self.pending_file, 'w')
+ cPickle.dump(pending_data, savefile)
+ savefile.close()
+ self.logger.info("Saved pending %s data" % self.name)
+ except:
+ err = sys.exc_info()[1]
+ self.logger.warning("Failed to save pending data: %s" % err)
+
+ def _load(self):
+ """Load any pending data from a file."""
+ if not os.path.exists(self.pending_file):
+ return True
+ pending_data = []
+ try:
+ savefile = open(self.pending_file, 'r')
+ pending_data = cPickle.load(savefile)
+ savefile.close()
+ except Exception:
+ e = sys.exc_info()[1]
+ self.logger.warning("Failed to load pending data: %s" % e)
+ return False
+ for (pmetadata, pdata) in pending_data:
+ # check that shutdown wasnt called early
+ if self.terminate.isSet():
+ return False
+
+ try:
+ while True:
+ try:
+ metadata = self.core.build_metadata(pmetadata)
+ break
+ except MetadataRuntimeError:
+ pass
+
+ self.terminate.wait(5)
+ if self.terminate.isSet():
+ return False
+
+ self.work_queue.put_nowait((metadata,
+ lxml.etree.XML(pdata,
+ parser=Bcfg2.Server.XMLParser)))
+ except Full:
+ self.logger.warning("Queue.Full: Failed to load queue data")
+ break
+ except lxml.etree.LxmlError:
+ lxml_error = sys.exc_info()[1]
+ self.logger.error("Unable to load saved interaction: %s" %
+ lxml_error)
+ except MetadataConsistencyError:
+ self.logger.error("Unable to load metadata for save "
+ "interaction: %s" % pmetadata)
+ try:
+ os.unlink(self.pending_file)
+ except:
+ self.logger.error("Failed to unlink save file: %s" %
+ self.pending_file)
+ self.logger.info("Loaded pending %s data" % self.name)
+ return True
+
+ def run(self):
+ if not self._load():
+ return
+ while not self.terminate.isSet() and self.work_queue != None:
+ try:
+ (client, xdata) = self.work_queue.get(block=True, timeout=2)
+ except Empty:
+ continue
+ except Exception:
+ e = sys.exc_info()[1]
+ self.logger.error("ThreadedStatistics: %s" % e)
+ continue
+ self.handle_statistic(client, xdata)
+ if self.work_queue != None and not self.work_queue.empty():
+ self._save()
+
+ def process_statistics(self, metadata, data):
+ try:
+ self.work_queue.put_nowait((metadata, copy.copy(data)))
+ except Full:
+ self.logger.warning("%s: Queue is full. Dropping interactions." %
+ self.name)
+
+ def handle_statistic(self, metadata, data):
+ """ Process the given XML statistics data for the specified
+ client object. This differs from the
+ :func:`Bcfg2.Server.Plugin.Statistics.process_statistics`
+ method only in that ThreadedStatistics first adds the data to
+ a queue, and then processes them in a separate thread.
+
+ :param metadata: The client metadata
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :param data: The statistics data
+ :type data: lxml.etree._Element
+ :return: None
+ """
+ raise NotImplementedError
+
+
+class PullSource(object):
+ def GetExtra(self, client):
+ return []
+
+ def GetCurrentEntry(self, client, e_type, e_name):
+ raise NotImplementedError
+
+
+class PullTarget(object):
+ def AcceptChoices(self, entry, metadata):
+ raise NotImplementedError
+
+ def AcceptPullData(self, specific, new_entry, verbose):
+ raise NotImplementedError
+
+
+class Decision(object):
+ """ Decision plugins produce decision lists for affecting which
+ entries are actually installed on clients. """
+
+ def GetDecisions(self, metadata, mode):
+ """ Return a list of tuples of ``(<entry type>, <entry
+ name>)`` to be used as the decision list for the given
+ client in the specified mode.
+
+ :param metadata: The client metadata
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :param mode: The decision mode ("whitelist" or "blacklist")
+ :type mode: string
+ :return: list of tuples
+ """
+ raise NotImplementedError
+
+
+class StructureValidator(object):
+ """ StructureValidator plugins can modify the list of structures
+ after it has been created but before the entries have been
+ concretely bound. """
+
+ def validate_structures(self, metadata, structures):
+ """ Given a list of structures (i.e., of tags that contain
+ entry tags), modify that list or the structures in it
+ in-place.
+
+ :param metadata: The client metadata
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :param config: A list of lxml.etree._Element objects
+ describing the structures for this client
+ :type config: list
+ :returns: None
+ :raises: Bcfg2.Server.Plugin.ValidationError
+ """
+ raise NotImplementedError
+
+
+class GoalValidator(object):
+ """ GoalValidator plugins can modify the concretely-bound configuration of
+ a client as a last stage before the configuration is sent to the
+ client. """
+
+ def validate_goals(self, metadata, config):
+ """ Given a monolithic XML document of the full configuration,
+ modify the document in-place.
+
+ :param metadata: The client metadata
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :param config: The full configuration for the client
+ :type config: lxml.etree._Element
+ :returns: None
+ :raises: Bcfg2.Server.Plugin.ValidationError
+ """
+ raise NotImplementedError
+
+
+class Version(object):
+ """ Version plugins interact with various version control systems. """
+
+ def get_revision(self):
+ """ Return the current revision of the Bcfg2 specification.
+ This will be included in the ``revision`` attribute of the
+ top-level tag of the XML configuration sent to the client.
+
+ :returns: string - the current version
+ """
+ raise NotImplementedError
+
+
+class ClientRunHooks(object):
+ """ ClientRunHooks can hook into various parts of a client run to
+ perform actions at various times without needing to pretend to be
+ a different plugin type. """
+
+ def start_client_run(self, metadata):
+ """ Invoked at the start of a client run, after all probe data
+ has been received and decision lists have been queried (if
+ applicable), but before the configuration is generated.
+
+ :param metadata: The client metadata object
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :returns: None
+ """
+ pass
+
+ def end_client_run(self, metadata):
+ """ Invoked at the end of a client run, immediately after
+ :class:`Bcfg2.Server.Plugin.GoalValidator` plugins have been run
+ and just before the configuration is returned to the client.
+
+ :param metadata: The client metadata object
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :returns: None
+ """
+ pass
+
+ def end_statistics(self, metadata):
+ """ Invoked after statistics are processed for a client.
+
+ :param metadata: The client metadata object
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :returns: None
+ """
+ pass
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py
new file mode 100644
index 000000000..9f2f618c9
--- /dev/null
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py
@@ -0,0 +1,83 @@
+import os
+import sys
+import logging
+from mock import Mock, MagicMock, patch
+from Bcfg2.Server.Plugin.base import *
+
+# add all parent testsuite directories to sys.path to allow (most)
+# relative imports in python 2.4
+path = os.path.dirname(__file__)
+while path != '/':
+ if os.path.basename(path).lower().startswith("test"):
+ sys.path.append(path)
+ if os.path.basename(path) == "testsuite":
+ break
+ path = os.path.dirname(path)
+from common import call, builtins, skip, skipIf, skipUnless, Bcfg2TestCase, \
+ patchIf, datastore
+
+
+class TestDebuggable(Bcfg2TestCase):
+ test_obj = Debuggable
+
+ def get_obj(self):
+ return self.test_obj()
+
+ def test__init(self):
+ d = self.get_obj()
+ self.assertIsInstance(d.logger, logging.Logger)
+ self.assertFalse(d.debug_flag)
+
+ @patch("Bcfg2.Server.Plugin.base.%s.debug_log" % test_obj.__name__)
+ def test_toggle_debug(self, mock_debug):
+ d = self.get_obj()
+ orig = d.debug_flag
+ d.toggle_debug()
+ self.assertNotEqual(orig, d.debug_flag)
+ self.assertTrue(mock_debug.called)
+
+ mock_debug.reset_mock()
+
+ changed = d.debug_flag
+ d.toggle_debug()
+ self.assertNotEqual(changed, d.debug_flag)
+ self.assertEqual(orig, d.debug_flag)
+ self.assertTrue(mock_debug.called)
+
+ def test_debug_log(self):
+ d = self.get_obj()
+ d.logger = Mock()
+ d.debug_flag = False
+ d.debug_log("test")
+ self.assertFalse(d.logger.error.called)
+
+ d.logger.reset_mock()
+ d.debug_log("test", flag=True)
+ self.assertTrue(d.logger.error.called)
+
+ d.logger.reset_mock()
+ d.debug_flag = True
+ d.debug_log("test")
+ self.assertTrue(d.logger.error.called)
+
+
+class TestPlugin(TestDebuggable):
+ test_obj = Plugin
+
+ def get_obj(self, core=None):
+ if core is None:
+ core = Mock()
+ return self.test_obj(core, datastore)
+
+ def test__init(self):
+ core = Mock()
+ p = self.get_obj(core=core)
+ self.assertEqual(p.data, os.path.join(datastore, p.name))
+ self.assertEqual(p.core, core)
+ self.assertIsInstance(p, Debuggable)
+
+ @patch("os.makedirs")
+ def test_init_repo(self, mock_makedirs):
+ self.test_obj.init_repo(datastore)
+ mock_makedirs.assert_called_with(os.path.join(datastore,
+ self.test_obj.name))
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testexceptions.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testexceptions.py
new file mode 100644
index 000000000..d2b72251e
--- /dev/null
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testexceptions.py
@@ -0,0 +1,47 @@
+import os
+import sys
+from mock import Mock, MagicMock, patch
+from Bcfg2.Server.Plugin.exceptions import *
+
+# add all parent testsuite directories to sys.path to allow (most)
+# relative imports in python 2.4
+path = os.path.dirname(__file__)
+while path != '/':
+ if os.path.basename(path).lower().startswith("test"):
+ sys.path.append(path)
+ if os.path.basename(path) == "testsuite":
+ break
+ path = os.path.dirname(path)
+from common import call, builtins, skip, skipIf, skipUnless, Bcfg2TestCase, \
+ patchIf, datastore
+
+
+class TestPluginInitError(Bcfg2TestCase):
+ """ placeholder for future tests """
+ pass
+
+
+class TestPluginExecutionError(Bcfg2TestCase):
+ """ placeholder for future tests """
+ pass
+
+
+class TestMetadataConsistencyError(Bcfg2TestCase):
+ """ placeholder for future tests """
+ pass
+
+
+class TestMetadataRuntimeError(Bcfg2TestCase):
+ """ placeholder for future tests """
+ pass
+
+
+class TestValidationError(Bcfg2TestCase):
+ """ placeholder for future tests """
+ pass
+
+
+class TestSpecificityError(Bcfg2TestCase):
+ """ placeholder for future tests """
+ pass
+
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
index 5410c550e..f19aa6b57 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugin.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
@@ -2,12 +2,11 @@ import os
import re
import sys
import copy
-import logging
import lxml.etree
import Bcfg2.Server
from Bcfg2.Compat import reduce
from mock import Mock, MagicMock, patch
-from Bcfg2.Server.Plugin import *
+from Bcfg2.Server.Plugin.helpers import *
# add all parent testsuite directories to sys.path to allow (most)
# relative imports in python 2.4
@@ -18,10 +17,10 @@ while path != '/':
if os.path.basename(path) == "testsuite":
break
path = os.path.dirname(path)
-from common import XI_NAMESPACE, XI, inPy3k, call, builtins, u, can_skip, \
- skip, skipIf, skipUnless, Bcfg2TestCase, DBModelTestCase, syncdb, \
- patchIf, datastore
-
+from common import XI_NAMESPACE, XI, call, builtins, skip, skipIf, skipUnless, \
+ Bcfg2TestCase, DBModelTestCase, syncdb, patchIf, datastore
+from Testbase import TestPlugin, TestDebuggable
+from Testinterfaces import TestGenerator
try:
re_type = re._pattern_type
@@ -79,82 +78,6 @@ class TestFunctions(Bcfg2TestCase):
name="/test"))
-class TestPluginInitError(Bcfg2TestCase):
- """ placeholder for future tests """
- pass
-
-
-class TestPluginExecutionError(Bcfg2TestCase):
- """ placeholder for future tests """
- pass
-
-
-class TestDebuggable(Bcfg2TestCase):
- test_obj = Debuggable
-
- def get_obj(self):
- return self.test_obj()
-
- def test__init(self):
- d = self.get_obj()
- self.assertIsInstance(d.logger, logging.Logger)
- self.assertFalse(d.debug_flag)
-
- @patch("Bcfg2.Server.Plugin.%s.debug_log" % test_obj.__name__)
- def test_toggle_debug(self, mock_debug):
- d = self.get_obj()
- orig = d.debug_flag
- d.toggle_debug()
- self.assertNotEqual(orig, d.debug_flag)
- self.assertTrue(mock_debug.called)
-
- mock_debug.reset_mock()
-
- changed = d.debug_flag
- d.toggle_debug()
- self.assertNotEqual(changed, d.debug_flag)
- self.assertEqual(orig, d.debug_flag)
- self.assertTrue(mock_debug.called)
-
- def test_debug_log(self):
- d = self.get_obj()
- d.logger = Mock()
- d.debug_flag = False
- d.debug_log("test")
- self.assertFalse(d.logger.error.called)
-
- d.logger.reset_mock()
- d.debug_log("test", flag=True)
- self.assertTrue(d.logger.error.called)
-
- d.logger.reset_mock()
- d.debug_flag = True
- d.debug_log("test")
- self.assertTrue(d.logger.error.called)
-
-
-class TestPlugin(TestDebuggable):
- test_obj = Plugin
-
- def get_obj(self, core=None):
- if core is None:
- core = Mock()
- return self.test_obj(core, datastore)
-
- def test__init(self):
- core = Mock()
- p = self.get_obj(core=core)
- self.assertEqual(p.data, os.path.join(datastore, p.name))
- self.assertEqual(p.core, core)
- self.assertIsInstance(p, Debuggable)
-
- @patch("os.makedirs")
- def test_init_repo(self, mock_makedirs):
- self.test_obj.init_repo(datastore)
- mock_makedirs.assert_called_with(os.path.join(datastore,
- self.test_obj.name))
-
-
class TestDatabaseBacked(TestPlugin):
test_obj = DatabaseBacked
@@ -170,7 +93,7 @@ class TestDatabaseBacked(TestPlugin):
db = self.get_obj(core)
self.assertFalse(db._use_db)
- Bcfg2.Server.Plugin.has_django = False
+ Bcfg2.Server.Plugin.helpers.has_django = False
core = Mock()
db = self.get_obj(core)
self.assertFalse(db._use_db)
@@ -179,7 +102,7 @@ class TestDatabaseBacked(TestPlugin):
core.setup.cfp.getboolean.return_value = True
db = self.get_obj(core)
self.assertFalse(db._use_db)
- Bcfg2.Server.Plugin.has_django = True
+ Bcfg2.Server.Plugin.helpers.has_django = True
class TestPluginDatabaseModel(Bcfg2TestCase):
@@ -187,335 +110,6 @@ class TestPluginDatabaseModel(Bcfg2TestCase):
pass
-class TestGenerator(Bcfg2TestCase):
- test_obj = Generator
-
- def test_HandlesEntry(self):
- pass
-
- def test_HandleEntry(self):
- pass
-
-
-class TestStructure(Bcfg2TestCase):
- test_obj = Structure
-
- def get_obj(self):
- return self.test_obj()
-
- def test_BuildStructures(self):
- s = self.get_obj()
- self.assertRaises(NotImplementedError,
- s.BuildStructures, None)
-
-
-class TestMetadata(Bcfg2TestCase):
- test_obj = Metadata
-
- def get_obj(self):
- return self.test_obj()
-
- def test_AuthenticateConnection(self):
- m = self.get_obj()
- self.assertRaises(NotImplementedError,
- m.AuthenticateConnection,
- None, None, None, (None, None))
-
- def test_get_initial_metadata(self):
- m = self.get_obj()
- self.assertRaises(NotImplementedError,
- m.get_initial_metadata, None)
-
- def test_merge_additional_data(self):
- m = self.get_obj()
- self.assertRaises(NotImplementedError,
- m.merge_additional_data, None, None, None)
-
- def test_merge_additional_groups(self):
- m = self.get_obj()
- self.assertRaises(NotImplementedError,
- m.merge_additional_groups, None, None)
-
-
-class TestConnector(Bcfg2TestCase):
- """ placeholder """
- def test_get_additional_groups(self):
- pass
-
- def test_get_additional_data(self):
- pass
-
-
-class TestProbing(Bcfg2TestCase):
- test_obj = Probing
-
- def get_obj(self):
- return self.test_obj()
-
- def test_GetProbes(self):
- p = self.get_obj()
- self.assertRaises(NotImplementedError,
- p.GetProbes, None)
-
- def test_ReceiveData(self):
- p = self.get_obj()
- self.assertRaises(NotImplementedError,
- p.ReceiveData, None, None)
-
-
-class TestStatistics(TestPlugin):
- test_obj = Statistics
-
- def get_obj(self, core=None):
- if core is None:
- core = Mock()
- return self.test_obj(core, datastore)
-
- def test_process_statistics(self):
- s = self.get_obj()
- self.assertRaises(NotImplementedError,
- s.process_statistics, None, None)
-
-
-class TestThreadedStatistics(TestStatistics):
- test_obj = ThreadedStatistics
- data = [("foo.example.com", "<foo/>"),
- ("bar.example.com", "<bar/>")]
-
- @patch("threading.Thread.start")
- def test__init(self, mock_start):
- core = Mock()
- ts = self.get_obj(core)
- mock_start.assert_any_call()
-
- @patch("%s.open" % builtins)
- @patch("%s.dump" % cPickle.__name__)
- @patch("Bcfg2.Server.Plugin.ThreadedStatistics.run", Mock())
- def test_save(self, mock_dump, mock_open):
- core = Mock()
- ts = self.get_obj(core)
- queue = Mock()
- queue.empty = Mock(side_effect=Empty)
- ts.work_queue = queue
-
- mock_open.side_effect = OSError
- # test that save does _not_ raise an exception even when
- # everything goes pear-shaped
- ts._save()
- queue.empty.assert_any_call()
- mock_open.assert_called_with(ts.pending_file, 'w')
-
- queue.reset_mock()
- mock_open.reset_mock()
-
- queue.data = []
- for hostname, xml in self.data:
- md = Mock()
- md.hostname = hostname
- queue.data.append((md, lxml.etree.XML(xml)))
- queue.empty.side_effect = lambda: len(queue.data) == 0
- queue.get_nowait = Mock(side_effect=lambda: queue.data.pop())
- mock_open.side_effect = None
-
- ts._save()
- queue.empty.assert_any_call()
- queue.get_nowait.assert_any_call()
- mock_open.assert_called_with(ts.pending_file, 'w')
- mock_open.return_value.close.assert_any_call()
- # the order of the queue data gets changed, so we have to
- # verify this call in an ugly way
- self.assertItemsEqual(mock_dump.call_args[0][0], self.data)
- self.assertEqual(mock_dump.call_args[0][1], mock_open.return_value)
-
- @patch("os.unlink")
- @patch("os.path.exists")
- @patch("%s.open" % builtins)
- @patch("lxml.etree.XML")
- @patch("%s.load" % cPickle.__name__)
- @patch("Bcfg2.Server.Plugin.ThreadedStatistics.run", Mock())
- def test_load(self, mock_load, mock_XML, mock_open, mock_exists,
- mock_unlink):
- core = Mock()
- core.terminate.isSet.return_value = False
- ts = self.get_obj(core)
-
- ts.work_queue = Mock()
- ts.work_queue.data = []
- def reset():
- core.reset_mock()
- mock_open.reset_mock()
- mock_exists.reset_mock()
- mock_unlink.reset_mock()
- mock_load.reset_mock()
- mock_XML.reset_mock()
- ts.work_queue.reset_mock()
- ts.work_queue.data = []
-
- mock_exists.return_value = False
- self.assertTrue(ts._load())
- mock_exists.assert_called_with(ts.pending_file)
-
- reset()
- mock_exists.return_value = True
- mock_open.side_effect = OSError
- self.assertFalse(ts._load())
- mock_exists.assert_called_with(ts.pending_file)
- mock_open.assert_called_with(ts.pending_file, 'r')
-
- reset()
- mock_open.side_effect = None
- mock_load.return_value = self.data
- ts.work_queue.put_nowait.side_effect = Full
- self.assertTrue(ts._load())
- mock_exists.assert_called_with(ts.pending_file)
- mock_open.assert_called_with(ts.pending_file, 'r')
- mock_open.return_value.close.assert_any_call()
- mock_load.assert_called_with(mock_open.return_value)
-
- reset()
- core.build_metadata.side_effect = lambda x: x
- mock_XML.side_effect = lambda x, parser=None: x
- ts.work_queue.put_nowait.side_effect = None
- self.assertTrue(ts._load())
- mock_exists.assert_called_with(ts.pending_file)
- mock_open.assert_called_with(ts.pending_file, 'r')
- mock_open.return_value.close.assert_any_call()
- mock_load.assert_called_with(mock_open.return_value)
- self.assertItemsEqual(mock_XML.call_args_list,
- [call(x, parser=Bcfg2.Server.XMLParser)
- for h, x in self.data])
- self.assertItemsEqual(ts.work_queue.put_nowait.call_args_list,
- [call((h, x)) for h, x in self.data])
- mock_unlink.assert_called_with(ts.pending_file)
-
- @patch("threading.Thread.start", Mock())
- @patch("Bcfg2.Server.Plugin.ThreadedStatistics._load")
- @patch("Bcfg2.Server.Plugin.ThreadedStatistics._save")
- @patch("Bcfg2.Server.Plugin.ThreadedStatistics.handle_statistic")
- def test_run(self, mock_handle, mock_save, mock_load):
- core = Mock()
- ts = self.get_obj(core)
- mock_load.return_value = True
- ts.work_queue = Mock()
-
- def reset():
- mock_handle.reset_mock()
- mock_save.reset_mock()
- mock_load.reset_mock()
- core.reset_mock()
- ts.work_queue.reset_mock()
- ts.work_queue.data = self.data[:]
- ts.work_queue.get_calls = 0
-
- reset()
-
- def get_rv(**kwargs):
- ts.work_queue.get_calls += 1
- try:
- return ts.work_queue.data.pop()
- except:
- raise Empty
- ts.work_queue.get.side_effect = get_rv
- def terminate_isset():
- # this lets the loop go on a few iterations with an empty
- # queue to test that it doesn't error out
- return ts.work_queue.get_calls > 3
- core.terminate.isSet.side_effect = terminate_isset
-
- ts.work_queue.empty.return_value = False
- ts.run()
- mock_load.assert_any_call()
- self.assertGreaterEqual(ts.work_queue.get.call_count, len(self.data))
- self.assertItemsEqual(mock_handle.call_args_list,
- [call(h, x) for h, x in self.data])
- mock_save.assert_any_call()
-
- @patch("copy.copy", Mock(side_effect=lambda x: x))
- @patch("Bcfg2.Server.Plugin.ThreadedStatistics.run", Mock())
- def test_process_statistics(self):
- core = Mock()
- ts = self.get_obj(core)
- ts.work_queue = Mock()
- ts.process_statistics(*self.data[0])
- ts.work_queue.put_nowait.assert_called_with(self.data[0])
-
- ts.work_queue.reset_mock()
- ts.work_queue.put_nowait.side_effect = Full
- # test that no exception is thrown
- ts.process_statistics(*self.data[0])
-
- def test_handle_statistic(self):
- ts = self.get_obj()
- self.assertRaises(NotImplementedError,
- ts.handle_statistic, None, None)
-
-
-class TestPullSource(Bcfg2TestCase):
- def test_GetCurrentEntry(self):
- ps = PullSource()
- self.assertRaises(NotImplementedError,
- ps.GetCurrentEntry, None, None, None)
-
-
-class TestPullTarget(Bcfg2TestCase):
- def test_AcceptChoices(self):
- pt = PullTarget()
- self.assertRaises(NotImplementedError,
- pt.AcceptChoices, None, None)
-
- def test_AcceptPullData(self):
- pt = PullTarget()
- self.assertRaises(NotImplementedError,
- pt.AcceptPullData, None, None, None)
-
-
-class TestDecision(Bcfg2TestCase):
- test_obj = Decision
-
- def get_obj(self):
- return self.test_obj()
-
- def test_GetDecisions(self):
- d = self.get_obj()
- self.assertRaises(NotImplementedError,
- d.GetDecisions, None, None)
-
-
-class TestValidationError(Bcfg2TestCase):
- """ placeholder for future tests """
- pass
-
-
-class TestStructureValidator(Bcfg2TestCase):
- def test_validate_structures(self):
- sv = StructureValidator()
- self.assertRaises(NotImplementedError,
- sv.validate_structures, None, None)
-
-
-class TestGoalValidator(Bcfg2TestCase):
- def test_validate_goals(self):
- gv = GoalValidator()
- self.assertRaises(NotImplementedError,
- gv.validate_goals, None, None)
-
-
-class TestVersion(Bcfg2TestCase):
- test_obj = Version
-
- def get_obj(self):
- return self.test_obj()
-
- def test_get_revision(self):
- d = self.get_obj()
- self.assertRaises(NotImplementedError, d.get_revision)
-
-
-class TestClientRunHooks(Bcfg2TestCase):
- """ placeholder for future tests """
- pass
-
-
class TestFileBacked(Bcfg2TestCase):
test_obj = FileBacked
path = os.path.join(datastore, "test")
@@ -569,15 +163,16 @@ class TestDirectoryBacked(Bcfg2TestCase):
# ensure that the child object has the correct interface
self.assertTrue(hasattr(self.test_obj.__child__, "HandleEvent"))
- @patch("Bcfg2.Server.Plugin.%s.add_directory_monitor" % test_obj.__name__,
- Mock())
+ @patch("Bcfg2.Server.Plugin.helpers.%s.add_directory_monitor" %
+ test_obj.__name__, Mock())
def get_obj(self, fam=None):
if fam is None:
fam = Mock()
return self.test_obj(os.path.join(datastore, self.test_obj.__name__),
fam)
- @patch("Bcfg2.Server.Plugin.%s.add_directory_monitor" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.add_directory_monitor" %
+ test_obj.__name__)
def test__init(self, mock_add_monitor):
db = self.test_obj(datastore, Mock())
mock_add_monitor.assert_called_with('')
@@ -662,8 +257,9 @@ class TestDirectoryBacked(Bcfg2TestCase):
db.entries[path].HandleEvent.assert_called_with(event)
@patch("os.path.isdir")
- @patch("Bcfg2.Server.Plugin.%s.add_entry" % test_obj.__name__)
- @patch("Bcfg2.Server.Plugin.%s.add_directory_monitor" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.add_entry" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.add_directory_monitor" %
+ test_obj.__name__)
def test_HandleEvent(self, mock_add_monitor, mock_add_entry, mock_isdir):
db = self.get_obj()
# a path with a leading / should never get into
@@ -906,7 +502,8 @@ class TestXMLFileBacked(TestFileBacked):
[call(f) for f in xdata.keys() if f != self.path])
@patch("lxml.etree._ElementTree", FakeElementTree)
- @patch("Bcfg2.Server.Plugin.%s._follow_xincludes" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s._follow_xincludes" %
+ test_obj.__name__)
def test_Index(self, mock_follow):
xfb = self.get_obj()
@@ -1094,7 +691,8 @@ class TestStructFile(TestXMLFileBacked):
self.assertTrue(inc("Other"))
- @patch("Bcfg2.Server.Plugin.%s._include_element" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s._include_element" %
+ test_obj.__name__)
def test__match(self, mock_include):
sf = self.get_obj()
metadata = Mock()
@@ -1122,7 +720,7 @@ class TestStructFile(TestXMLFileBacked):
for el in standalone:
self.assertXMLEqual(el, sf._match(el, metadata)[0])
- @patch("Bcfg2.Server.Plugin.%s._match" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s._match" % test_obj.__name__)
def test_Match(self, mock_match):
sf = self.get_obj()
metadata = Mock()
@@ -1152,7 +750,8 @@ class TestStructFile(TestXMLFileBacked):
xexpected.extend(expected)
self.assertXMLEqual(xactual, xexpected)
- @patch("Bcfg2.Server.Plugin.%s._include_element" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s._include_element" %
+ test_obj.__name__)
def test__xml_match(self, mock_include):
sf = self.get_obj()
metadata = Mock()
@@ -1174,7 +773,7 @@ class TestStructFile(TestXMLFileBacked):
expected.extend(standalone)
self.assertXMLEqual(actual, expected)
- @patch("Bcfg2.Server.Plugin.%s._xml_match" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s._xml_match" % test_obj.__name__)
def test_Match(self, mock_xml_match):
sf = self.get_obj()
metadata = Mock()
@@ -1210,7 +809,8 @@ class TestINode(Bcfg2TestCase):
# atomically, we do this umpteen times in order to test with
# different data. this convenience method makes this a little
# easier. fun fun fun.
- @patch("Bcfg2.Server.Plugin.%s._load_children" % test_obj.__name__, Mock())
+ @patch("Bcfg2.Server.Plugin.helpers.%s._load_children" %
+ test_obj.__name__, Mock())
def _get_inode(self, data, idict):
return self.test_obj(data, idict)
@@ -1268,7 +868,7 @@ class TestINode(Bcfg2TestCase):
self.assertItemsEqual(self.test_obj.containers,
self.test_obj.nraw.keys())
- @patch("Bcfg2.Server.Plugin.INode._load_children")
+ @patch("Bcfg2.Server.Plugin.helpers.INode._load_children")
def test__init(self, mock_load_children):
data = lxml.etree.Element("Bogus")
# called with no parent, should not raise an exception; it's a
@@ -1329,7 +929,8 @@ class TestINode(Bcfg2TestCase):
inode = self._get_inode(data, idict)
- @patch("Bcfg2.Server.Plugin.%s.__init__" % inode.__class__.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.__init__" %
+ inode.__class__.__name__)
def inner(mock_init):
mock_init.return_value = None
inode._load_children(data, idict)
@@ -1352,7 +953,8 @@ class TestINode(Bcfg2TestCase):
inode = self._get_inode(data, idict)
inode.ignore = []
- @patch("Bcfg2.Server.Plugin.%s.__init__" % inode.__class__.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.__init__" %
+ inode.__class__.__name__)
def inner2(mock_init):
mock_init.return_value = None
inode._load_children(data, idict)
@@ -1378,7 +980,8 @@ class TestINode(Bcfg2TestCase):
inode = self._get_inode(data, idict)
- @patch("Bcfg2.Server.Plugin.%s.__init__" % inode.__class__.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.__init__" %
+ inode.__class__.__name__)
def inner3(mock_init):
mock_init.return_value = None
inode._load_children(data, idict)
@@ -1511,7 +1114,7 @@ class TestXMLSrc(TestXMLFileBacked):
self.assertEqual(xsrc.pnode, xsrc.__node__.return_value)
self.assertEqual(xsrc.cache, None)
- @patch("Bcfg2.Server.Plugin.XMLSrc.HandleEvent")
+ @patch("Bcfg2.Server.Plugin.helpers.XMLSrc.HandleEvent")
def test_Cache(self, mock_HandleEvent):
xsrc = self.get_obj("/test/foo.xml")
metadata = Mock()
@@ -1547,7 +1150,8 @@ class TestXMLDirectoryBacked(TestDirectoryBacked):
class TestPrioDir(TestPlugin, TestGenerator, TestXMLDirectoryBacked):
test_obj = PrioDir
- @patch("Bcfg2.Server.Plugin.%s.add_directory_monitor" % test_obj.__name__,
+ @patch("Bcfg2.Server.Plugin.helpers.%s.add_directory_monitor" %
+ test_obj.__name__,
Mock())
def get_obj(self, core=None):
if core is None:
@@ -1557,7 +1161,8 @@ class TestPrioDir(TestPlugin, TestGenerator, TestXMLDirectoryBacked):
def test_HandleEvent(self):
TestXMLDirectoryBacked.test_HandleEvent(self)
- @patch("Bcfg2.Server.Plugin.XMLDirectoryBacked.HandleEvent", Mock())
+ @patch("Bcfg2.Server.Plugin.helpers.XMLDirectoryBacked.HandleEvent",
+ Mock())
def inner():
pd = self.get_obj()
test1 = Mock()
@@ -1666,11 +1271,6 @@ class TestPrioDir(TestPlugin, TestGenerator, TestXMLDirectoryBacked):
pd.get_attrs, entry, metadata)
-class TestSpecificityError(Bcfg2TestCase):
- """ placeholder for future tests """
- pass
-
-
class TestSpecificity(Bcfg2TestCase):
test_obj = Specificity
@@ -1842,7 +1442,7 @@ class TestEntrySet(TestDebuggable):
for i in items.values():
i.specific.matches.assert_called_with(metadata)
- @patch("Bcfg2.Server.Plugin.%s.get_matching" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.get_matching" % test_obj.__name__)
def test_best_matching(self, mock_get_matching):
eset = self.get_obj()
metadata = Mock()
@@ -1901,9 +1501,9 @@ class TestEntrySet(TestDebuggable):
self.assertEqual(eset.best_matching(metadata), expected)
mock_get_matching.assert_called_with(metadata)
- @patch("Bcfg2.Server.Plugin.%s.entry_init" % test_obj.__name__)
- @patch("Bcfg2.Server.Plugin.%s.reset_metadata" % test_obj.__name__)
- @patch("Bcfg2.Server.Plugin.%s.update_metadata" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.entry_init" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.reset_metadata" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.update_metadata" % test_obj.__name__)
def test_handle_event(self, mock_update_md, mock_reset_md, mock_init):
def reset():
mock_update_md.reset_mock()
@@ -1962,7 +1562,7 @@ class TestEntrySet(TestDebuggable):
eset.handle_event(event)
self.assertNotIn("test.txt", eset.entries)
- @patch("Bcfg2.Server.Plugin.%s.specificity_from_filename" %
+ @patch("Bcfg2.Server.Plugin.helpers.%s.specificity_from_filename" %
test_obj.__name__)
def test_entry_init(self, mock_spec):
eset = self.get_obj()
@@ -2010,7 +1610,7 @@ class TestEntrySet(TestDebuggable):
mock_spec.assert_called_with("test3.txt", specific=None)
self.assertFalse(eset.entry_type.called)
- @patch("Bcfg2.Server.Plugin.Specificity")
+ @patch("Bcfg2.Server.Plugin.helpers.Specificity")
def test_specificity_from_filename(self, mock_spec):
def test(eset, fname, **kwargs):
mock_spec.reset_mock()
@@ -2053,7 +1653,7 @@ class TestEntrySet(TestDebuggable):
fails(eset, ppath + ".H_")
@patch("%s.open" % builtins)
- @patch("Bcfg2.Server.Plugin.InfoXML")
+ @patch("Bcfg2.Server.Plugin.helpers.InfoXML")
def test_update_metadata(self, mock_InfoXML, mock_open):
eset = self.get_obj()
@@ -2107,7 +1707,7 @@ class TestEntrySet(TestDebuggable):
eset.reset_metadata(event)
self.assertItemsEqual(eset.metadata, default_file_metadata)
- @patch("Bcfg2.Server.Plugin.bind_info")
+ @patch("Bcfg2.Server.Plugin.helpers.bind_info")
def test_bind_info_to_entry(self, mock_bind_info):
eset = self.get_obj()
entry = Mock()
@@ -2117,8 +1717,9 @@ class TestEntrySet(TestDebuggable):
infoxml=eset.infoxml,
default=eset.metadata)
- @patch("Bcfg2.Server.Plugin.%s.best_matching" % test_obj.__name__)
- @patch("Bcfg2.Server.Plugin.%s.bind_info_to_entry" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.best_matching" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.bind_info_to_entry" %
+ test_obj.__name__)
def test_bind_entry(self, mock_bind_info, mock_best_matching):
eset = self.get_obj()
entry = Mock()
@@ -2133,11 +1734,13 @@ class TestEntrySet(TestDebuggable):
class TestGroupSpool(TestPlugin, TestGenerator):
test_obj = GroupSpool
- @patch("Bcfg2.Server.Plugin.%s.AddDirectoryMonitor" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.AddDirectoryMonitor" %
+ test_obj.__name__)
def get_obj(self, core=None):
return TestPlugin.get_obj(self, core=core)
- @patch("Bcfg2.Server.Plugin.%s.AddDirectoryMonitor" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.AddDirectoryMonitor" %
+ test_obj.__name__)
def test__init(self, mock_Add):
core = Mock()
gs = self.test_obj(core, datastore)
@@ -2146,9 +1749,10 @@ class TestGroupSpool(TestPlugin, TestGenerator):
@patch("os.path.isdir")
@patch("os.path.isfile")
- @patch("Bcfg2.Server.Plugin.%s.event_id" % test_obj.__name__)
- @patch("Bcfg2.Server.Plugin.%s.event_path" % test_obj.__name__)
- @patch("Bcfg2.Server.Plugin.%s.AddDirectoryMonitor" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.event_id" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.event_path" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.AddDirectoryMonitor" %
+ test_obj.__name__)
def test_add_entry(self, mock_Add, mock_event_path, mock_event_id,
mock_isfile, mock_isdir):
gs = self.get_obj()
@@ -2226,7 +1830,7 @@ class TestGroupSpool(TestPlugin, TestGenerator):
event.filename))
@patch("os.path.isdir")
- @patch("Bcfg2.Server.Plugin.%s.event_path" % test_obj.__name__)
+ @patch("Bcfg2.Server.Plugin.helpers.%s.event_path" % test_obj.__name__)
def test_event_id(self, mock_event_path, mock_isdir):
gs = self.get_obj()
@@ -2260,7 +1864,7 @@ class TestGroupSpool(TestPlugin, TestGenerator):
"/bar": Mock(),
"/baz/quux": Mock()}
- @patch("Bcfg2.Server.Plugin.Plugin.toggle_debug")
+ @patch("Bcfg2.Server.Plugin.base.Plugin.toggle_debug")
def inner(mock_debug):
gs.toggle_debug()
mock_debug.assert_called_with(gs)
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testinterfaces.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testinterfaces.py
new file mode 100644
index 000000000..01d7db067
--- /dev/null
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testinterfaces.py
@@ -0,0 +1,342 @@
+import os
+import sys
+import lxml.etree
+import Bcfg2.Server
+from mock import Mock, MagicMock, patch
+from Bcfg2.Server.Plugin.interfaces import *
+
+# add all parent testsuite directories to sys.path to allow (most)
+# relative imports in python 2.4
+path = os.path.dirname(__file__)
+while path != '/':
+ if os.path.basename(path).lower().startswith("test"):
+ sys.path.append(path)
+ if os.path.basename(path) == "testsuite":
+ break
+ path = os.path.dirname(path)
+from common import call, builtins, skip, skipIf, skipUnless, Bcfg2TestCase, \
+ patchIf, datastore
+from Testbase import TestPlugin
+
+class TestGenerator(Bcfg2TestCase):
+ test_obj = Generator
+
+ def test_HandlesEntry(self):
+ pass
+
+ def test_HandleEntry(self):
+ pass
+
+
+class TestStructure(Bcfg2TestCase):
+ test_obj = Structure
+
+ def get_obj(self):
+ return self.test_obj()
+
+ def test_BuildStructures(self):
+ s = self.get_obj()
+ self.assertRaises(NotImplementedError,
+ s.BuildStructures, None)
+
+
+class TestMetadata(Bcfg2TestCase):
+ test_obj = Metadata
+
+ def get_obj(self):
+ return self.test_obj()
+
+ def test_AuthenticateConnection(self):
+ m = self.get_obj()
+ self.assertRaises(NotImplementedError,
+ m.AuthenticateConnection,
+ None, None, None, (None, None))
+
+ def test_get_initial_metadata(self):
+ m = self.get_obj()
+ self.assertRaises(NotImplementedError,
+ m.get_initial_metadata, None)
+
+ def test_merge_additional_data(self):
+ m = self.get_obj()
+ self.assertRaises(NotImplementedError,
+ m.merge_additional_data, None, None, None)
+
+ def test_merge_additional_groups(self):
+ m = self.get_obj()
+ self.assertRaises(NotImplementedError,
+ m.merge_additional_groups, None, None)
+
+
+class TestConnector(Bcfg2TestCase):
+ """ placeholder """
+ def test_get_additional_groups(self):
+ pass
+
+ def test_get_additional_data(self):
+ pass
+
+
+class TestProbing(Bcfg2TestCase):
+ test_obj = Probing
+
+ def get_obj(self):
+ return self.test_obj()
+
+ def test_GetProbes(self):
+ p = self.get_obj()
+ self.assertRaises(NotImplementedError,
+ p.GetProbes, None)
+
+ def test_ReceiveData(self):
+ p = self.get_obj()
+ self.assertRaises(NotImplementedError,
+ p.ReceiveData, None, None)
+
+
+class TestStatistics(TestPlugin):
+ test_obj = Statistics
+
+ def get_obj(self, core=None):
+ if core is None:
+ core = Mock()
+ return self.test_obj(core, datastore)
+
+ def test_process_statistics(self):
+ s = self.get_obj()
+ self.assertRaises(NotImplementedError,
+ s.process_statistics, None, None)
+
+
+class TestThreadedStatistics(TestStatistics):
+ test_obj = ThreadedStatistics
+ data = [("foo.example.com", "<foo/>"),
+ ("bar.example.com", "<bar/>")]
+
+ @patch("threading.Thread.start")
+ def test__init(self, mock_start):
+ core = Mock()
+ ts = self.get_obj(core)
+ mock_start.assert_any_call()
+
+ @patch("%s.open" % builtins)
+ @patch("%s.dump" % cPickle.__name__)
+ @patch("Bcfg2.Server.Plugin.interfaces.ThreadedStatistics.run", Mock())
+ def test_save(self, mock_dump, mock_open):
+ core = Mock()
+ ts = self.get_obj(core)
+ queue = Mock()
+ queue.empty = Mock(side_effect=Empty)
+ ts.work_queue = queue
+
+ mock_open.side_effect = OSError
+ # test that save does _not_ raise an exception even when
+ # everything goes pear-shaped
+ ts._save()
+ queue.empty.assert_any_call()
+ mock_open.assert_called_with(ts.pending_file, 'w')
+
+ queue.reset_mock()
+ mock_open.reset_mock()
+
+ queue.data = []
+ for hostname, xml in self.data:
+ md = Mock()
+ md.hostname = hostname
+ queue.data.append((md, lxml.etree.XML(xml)))
+ queue.empty.side_effect = lambda: len(queue.data) == 0
+ queue.get_nowait = Mock(side_effect=lambda: queue.data.pop())
+ mock_open.side_effect = None
+
+ ts._save()
+ queue.empty.assert_any_call()
+ queue.get_nowait.assert_any_call()
+ mock_open.assert_called_with(ts.pending_file, 'w')
+ mock_open.return_value.close.assert_any_call()
+ # the order of the queue data gets changed, so we have to
+ # verify this call in an ugly way
+ self.assertItemsEqual(mock_dump.call_args[0][0], self.data)
+ self.assertEqual(mock_dump.call_args[0][1], mock_open.return_value)
+
+ @patch("os.unlink")
+ @patch("os.path.exists")
+ @patch("%s.open" % builtins)
+ @patch("lxml.etree.XML")
+ @patch("%s.load" % cPickle.__name__)
+ @patch("Bcfg2.Server.Plugin.interfaces.ThreadedStatistics.run", Mock())
+ def test_load(self, mock_load, mock_XML, mock_open, mock_exists,
+ mock_unlink):
+ core = Mock()
+ core.terminate.isSet.return_value = False
+ ts = self.get_obj(core)
+
+ ts.work_queue = Mock()
+ ts.work_queue.data = []
+ def reset():
+ core.reset_mock()
+ mock_open.reset_mock()
+ mock_exists.reset_mock()
+ mock_unlink.reset_mock()
+ mock_load.reset_mock()
+ mock_XML.reset_mock()
+ ts.work_queue.reset_mock()
+ ts.work_queue.data = []
+
+ mock_exists.return_value = False
+ self.assertTrue(ts._load())
+ mock_exists.assert_called_with(ts.pending_file)
+
+ reset()
+ mock_exists.return_value = True
+ mock_open.side_effect = OSError
+ self.assertFalse(ts._load())
+ mock_exists.assert_called_with(ts.pending_file)
+ mock_open.assert_called_with(ts.pending_file, 'r')
+
+ reset()
+ mock_open.side_effect = None
+ mock_load.return_value = self.data
+ ts.work_queue.put_nowait.side_effect = Full
+ self.assertTrue(ts._load())
+ mock_exists.assert_called_with(ts.pending_file)
+ mock_open.assert_called_with(ts.pending_file, 'r')
+ mock_open.return_value.close.assert_any_call()
+ mock_load.assert_called_with(mock_open.return_value)
+
+ reset()
+ core.build_metadata.side_effect = lambda x: x
+ mock_XML.side_effect = lambda x, parser=None: x
+ ts.work_queue.put_nowait.side_effect = None
+ self.assertTrue(ts._load())
+ mock_exists.assert_called_with(ts.pending_file)
+ mock_open.assert_called_with(ts.pending_file, 'r')
+ mock_open.return_value.close.assert_any_call()
+ mock_load.assert_called_with(mock_open.return_value)
+ self.assertItemsEqual(mock_XML.call_args_list,
+ [call(x, parser=Bcfg2.Server.XMLParser)
+ for h, x in self.data])
+ self.assertItemsEqual(ts.work_queue.put_nowait.call_args_list,
+ [call((h, x)) for h, x in self.data])
+ mock_unlink.assert_called_with(ts.pending_file)
+
+ @patch("threading.Thread.start", Mock())
+ @patch("Bcfg2.Server.Plugin.interfaces.ThreadedStatistics._load")
+ @patch("Bcfg2.Server.Plugin.interfaces.ThreadedStatistics._save")
+ @patch("Bcfg2.Server.Plugin.interfaces.ThreadedStatistics.handle_statistic")
+ def test_run(self, mock_handle, mock_save, mock_load):
+ core = Mock()
+ ts = self.get_obj(core)
+ mock_load.return_value = True
+ ts.work_queue = Mock()
+
+ def reset():
+ mock_handle.reset_mock()
+ mock_save.reset_mock()
+ mock_load.reset_mock()
+ core.reset_mock()
+ ts.work_queue.reset_mock()
+ ts.work_queue.data = self.data[:]
+ ts.work_queue.get_calls = 0
+
+ reset()
+
+ def get_rv(**kwargs):
+ ts.work_queue.get_calls += 1
+ try:
+ return ts.work_queue.data.pop()
+ except:
+ raise Empty
+ ts.work_queue.get.side_effect = get_rv
+ def terminate_isset():
+ # this lets the loop go on a few iterations with an empty
+ # queue to test that it doesn't error out
+ return ts.work_queue.get_calls > 3
+ core.terminate.isSet.side_effect = terminate_isset
+
+ ts.work_queue.empty.return_value = False
+ ts.run()
+ mock_load.assert_any_call()
+ self.assertGreaterEqual(ts.work_queue.get.call_count, len(self.data))
+ self.assertItemsEqual(mock_handle.call_args_list,
+ [call(h, x) for h, x in self.data])
+ mock_save.assert_any_call()
+
+ @patch("copy.copy", Mock(side_effect=lambda x: x))
+ @patch("Bcfg2.Server.Plugin.interfaces.ThreadedStatistics.run", Mock())
+ def test_process_statistics(self):
+ core = Mock()
+ ts = self.get_obj(core)
+ ts.work_queue = Mock()
+ ts.process_statistics(*self.data[0])
+ ts.work_queue.put_nowait.assert_called_with(self.data[0])
+
+ ts.work_queue.reset_mock()
+ ts.work_queue.put_nowait.side_effect = Full
+ # test that no exception is thrown
+ ts.process_statistics(*self.data[0])
+
+ def test_handle_statistic(self):
+ ts = self.get_obj()
+ self.assertRaises(NotImplementedError,
+ ts.handle_statistic, None, None)
+
+
+class TestPullSource(Bcfg2TestCase):
+ def test_GetCurrentEntry(self):
+ ps = PullSource()
+ self.assertRaises(NotImplementedError,
+ ps.GetCurrentEntry, None, None, None)
+
+
+class TestPullTarget(Bcfg2TestCase):
+ def test_AcceptChoices(self):
+ pt = PullTarget()
+ self.assertRaises(NotImplementedError,
+ pt.AcceptChoices, None, None)
+
+ def test_AcceptPullData(self):
+ pt = PullTarget()
+ self.assertRaises(NotImplementedError,
+ pt.AcceptPullData, None, None, None)
+
+
+class TestDecision(Bcfg2TestCase):
+ test_obj = Decision
+
+ def get_obj(self):
+ return self.test_obj()
+
+ def test_GetDecisions(self):
+ d = self.get_obj()
+ self.assertRaises(NotImplementedError,
+ d.GetDecisions, None, None)
+
+
+class TestStructureValidator(Bcfg2TestCase):
+ def test_validate_structures(self):
+ sv = StructureValidator()
+ self.assertRaises(NotImplementedError,
+ sv.validate_structures, None, None)
+
+
+class TestGoalValidator(Bcfg2TestCase):
+ def test_validate_goals(self):
+ gv = GoalValidator()
+ self.assertRaises(NotImplementedError,
+ gv.validate_goals, None, None)
+
+
+class TestVersion(Bcfg2TestCase):
+ test_obj = Version
+
+ def get_obj(self):
+ return self.test_obj()
+
+ def test_get_revision(self):
+ d = self.get_obj()
+ self.assertRaises(NotImplementedError, d.get_revision)
+
+
+class TestClientRunHooks(Bcfg2TestCase):
+ """ placeholder for future tests """
+ pass
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/__init__.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/__init__.py
new file mode 100644
index 000000000..d86cf1079
--- /dev/null
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/__init__.py
@@ -0,0 +1,17 @@
+import os
+import sys
+
+# add all parent testsuite directories to sys.path to allow (most)
+# relative imports in python 2.4
+path = os.path.dirname(__file__)
+while path != "/":
+ if os.path.basename(path).lower().startswith("test"):
+ sys.path.append(path)
+ if os.path.basename(path) == "testsuite":
+ break
+ path = os.path.dirname(path)
+
+from Testbase import *
+from Testinterfaces import *
+from Testhelpers import *
+from Testexceptions import *