From 460a1c2a0f1caa7eb2043ad10ba64b8b55e43844 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 6 Sep 2012 08:21:35 -0400 Subject: documented base plugin interfaces --- src/lib/Bcfg2/Server/Plugin.py | 441 ++++++++++++++++++++++---- src/lib/Bcfg2/Server/Plugins/ServiceCompat.py | 31 +- 2 files changed, 398 insertions(+), 74 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Server/Plugin.py b/src/lib/Bcfg2/Server/Plugin.py index 80537c200..0b2f7cee0 100644 --- a/src/lib/Bcfg2/Server/Plugin.py +++ b/src/lib/Bcfg2/Server/Plugin.py @@ -58,12 +58,14 @@ def bind_info(entry, metadata, infoxml=None, default=default_file_metadata): class PluginInitError(Exception): - """Error raised in cases of Plugin initialization errors.""" + """Error raised in cases of :class:`Bcfg2.Server.Plugin.Plugin` + initialization errors.""" pass class PluginExecutionError(Exception): - """Error raised in case of Plugin execution errors.""" + """Error raised in case of :class:`Bcfg2.Server.Plugin.Plugin` + execution errors.""" pass @@ -73,13 +75,17 @@ class MetadataConsistencyError(Exception): class MetadataRuntimeError(Exception): - """This error is raised when the metadata engine - is called prior to reading enough data. - """ + """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): @@ -90,6 +96,10 @@ class Debuggable(object): 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), @@ -97,37 +107,54 @@ class Debuggable(object): 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): - """This is the base class for all Bcfg2 Server plugins. - Several attributes must be defined in the subclass: - name : the name of the plugin - __author__ : the author/contact for the plugin - - Plugins can provide three basic types of functionality: - - Structure creation (overloading BuildStructures) - - Configuration entry binding (overloading HandlesEntry, or loads the Entries table) - - Data collection (overloading GetProbes/ReceiveData) - """ + """ 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 = [] - # Default sort_order to 500. 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. + #: 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 - :param datastore: the filesystem path of Bcfg2's repository + """ 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 = {} @@ -138,9 +165,19 @@ class Plugin(Debuggable): @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): @@ -168,84 +205,292 @@ class PluginDatabaseModel(object): class Generator(object): - """Generator plugins contribute to literal client - configurations.""" + """ 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): - """This is the slow path method for routing configuration - binding requests.""" + """ 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): - """This is the slow-path handler for configuration entry binding.""" + """ 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.""" + """ 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): - """Return a list of abstract goal structures for client.""" + """ Build a list of lxml.etree._Element objects that will be + added to the top-level ```` tag of the client + configuration. Consequently, each object in the list returned + by ``BuildStructures()`` must consist of a container tag + (e.g., ```` or ````) 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): - """Create viz str for viz admin mode.""" + """ Return a string containing a graphviz document that maps + out the Metadata for :ref:`bcfg2-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 ``(, )`` + :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 ``(, )`` + :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 ``(, + )`` + :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.""" + """ Connector plugins augment client metadata instances with + additional data, additional groups, or both. """ + def get_additional_groups(self, metadata): - """Determine additional groups for 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): - """Determine additional data for metadata instances.""" + """ 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): - """Signal probe capability for this plugin.""" + """ Probing plugins can collect data from clients and process it. + """ + def GetProbes(self, metadata): - """Return a set of probes for execution on client.""" + """ 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): - """Receive probe results pertaining to client.""" + """ 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): - """Signal statistics handling capability.""" + """ 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): - pass + """ 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): - """Threaded statistics handling capability.""" + """ ThreadedStatistics plugins process client statistics in a + separate thread. """ + def __init__(self, core, datastore): Statistics.__init__(self, core, datastore) threading.Thread.__init__(self) @@ -257,7 +502,7 @@ class ThreadedStatistics(Statistics, threading.Thread): self.daemon = False self.start() - def save(self): + def _save(self): """Save any pending data to a file.""" pending_data = [] try: @@ -283,7 +528,7 @@ class ThreadedStatistics(Statistics, threading.Thread): err = sys.exc_info()[1] self.logger.warning("Failed to save pending data: %s" % err) - def load(self): + def _load(self): """Load any pending data from a file.""" if not os.path.exists(self.pending_file): return True @@ -335,7 +580,7 @@ class ThreadedStatistics(Statistics, threading.Thread): return True def run(self): - if not self.load(): + if not self._load(): return while not self.terminate.isSet() and self.work_queue != None: try: @@ -348,7 +593,7 @@ class ThreadedStatistics(Statistics, threading.Thread): continue self.handle_statistic(client, xdata) if self.work_queue != None and not self.work_queue.empty(): - self.save() + self._save() def process_statistics(self, metadata, data): try: @@ -358,8 +603,19 @@ class ThreadedStatistics(Statistics, threading.Thread): self.name) def handle_statistic(self, metadata, data): - """Handle stats here.""" - pass + """ 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): @@ -375,51 +631,120 @@ class PullTarget(object): raise NotImplementedError def AcceptPullData(self, specific, new_entry, verbose): - """This is the null per-plugin implementation - of bcfg2-admin pull.""" raise NotImplementedError class Decision(object): - """Signal decision handling capability.""" + """ Decision plugins produce decision lists for affecting which + entries are actually installed on clients. """ + def GetDecisions(self, metadata, mode): - return [] + """ Return a list of tuples of ``(, )`` 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): - pass + """ Exception raised by + :class:`Bcfg2.Server.Plugin.StructureValidator` and + :class:`Bcfg2.Server.Plugin.GoalValidator` objects """ class StructureValidator(object): - """Validate/modify goal structures.""" + """ 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): - """Validate/modify configuration goals.""" - def validate_goals(self, metadata, goals): + """ 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): - """Interact with various version control systems.""" + """ Version plugins interact with various version control systems. """ + def get_revision(self): - return [] + """ 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. - def commit_data(self, file_list, comment=None): - pass + :returns: string - the current version + """ + raise NotImplementedError class ClientRunHooks(object): - """ Provides hooks to interact with client runs """ + """ 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 diff --git a/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py b/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py index aad92b7c7..f1309412a 100644 --- a/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py +++ b/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py @@ -1,7 +1,7 @@ import Bcfg2.Server.Plugin class ServiceCompat(Bcfg2.Server.Plugin.Plugin, - Bcfg2.Server.Plugin.StructureValidator): + Bcfg2.Server.Plugin.GoalValidator): """ Use old-style service modes for older clients """ name = 'ServiceCompat' __author__ = 'bcfg-dev@mcs.anl.gov' @@ -9,24 +9,23 @@ class ServiceCompat(Bcfg2.Server.Plugin.Plugin, ('interactive', 'true'): 'interactive_only', ('false', 'false'): 'manual'} - def validate_structures(self, metadata, structures): + def validate_goals(self, metadata, config): """ Apply defaults """ if metadata.version_info and metadata.version_info > (1, 3, 0, '', 0): # do not care about a client that is _any_ 1.3.0 release # (including prereleases and RCs) return - for struct in structures: - for entry in struct.xpath("//BoundService|//Service"): - mode_key = (entry.get("restart", "true").lower(), - entry.get("install", "true").lower()) - try: - mode = self.mode_map[mode_key] - except KeyError: - self.logger.info("Could not map restart and install " - "settings of %s:%s to an old-style " - "Service mode for %s; using 'manual'" % - (entry.tag, entry.get("name"), - metadata.hostname)) - mode = "manual" - entry.set("mode", mode) + for entry in config.xpath("//BoundService|//Service"): + mode_key = (entry.get("restart", "true").lower(), + entry.get("install", "true").lower()) + try: + mode = self.mode_map[mode_key] + except KeyError: + self.logger.info("Could not map restart and install settings " + "of %s:%s to an old-style Service mode for " + "%s; using 'manual'" % + (entry.tag, entry.get("name"), + metadata.hostname)) + mode = "manual" + entry.set("mode", mode) -- cgit v1.2.3-1-g7c22