diff options
-rw-r--r-- | doc/development/plugins.txt | 127 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugin/__init__.py | 11 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugin/base.py | 106 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugin/exceptions.py | 36 | ||||
-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.py | 548 | ||||
-rw-r--r-- | testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py | 83 | ||||
-rw-r--r-- | testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testexceptions.py | 47 | ||||
-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.py | 342 | ||||
-rw-r--r-- | testsuite/Testsrc/Testlib/TestServer/TestPlugin/__init__.py | 17 |
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 * |