class LdapQuery(object): """ Query referencing an LdapConnection and providing several methods for query manipulation. """ #: Name of the Query, used to register it in additional data. name = ClassName() base = "" scope = "sub" filter = "(objectClass=*)" attrs = None connection = None result = None def __unicode__(self): return "LdapQuery: %s" % self.name def is_applicable(self, metadata): # pylint: disable=W0613 """ Check is the query should be executed for a given metadata object. :param metadata: The client metadata :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata """ return True def prepare_query(self, metadata, **kwargs): # pylint: disable=W0613 """ Prepares the query based on the client metadata. You can for example modify the filter based on the client hostname. :param metadata: The client metadata :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata """ pass def process_result(self, metadata, **kwargs): # pylint: disable=W0613 """ Post-process the query result. :param metadata: The client metadata :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata """ return self.result def get_result(self, metadata, **kwargs): """ Handle the perparation, execution and processing of the query. :param metadata: The client metadata :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata :raises: :class:`Bcfg2.Server.Plugin.exceptions.PluginExecutionError` """ if self.connection is not None: self.prepare_query(metadata, **kwargs) self.result = self.connection.run_query(self) self.result = self.process_result(metadata, **kwargs) else: raise Bcfg2.Server.Plugin.PluginExecutionError( 'No connection defined for %s' % self.name) return self.result
class Plugin(Debuggable): """ The base class for all Bcfg2 Server plugins. """ #: The name of the plugin. name = ClassName() #: The email address of the plugin author. __author__ = '*****@*****.**' #: 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 #: List of names of methods to be exposed as XML-RPC functions __rmi__ = Debuggable.__rmi__ def __init__(self, core, datastore): """ :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: :exc:`OSError` if adding a file monitor failed; :class:`Bcfg2.Server.Plugin.exceptions.PluginInitError` on other errors .. autoattribute:: Bcfg2.Server.Plugin.base.Debuggable.__rmi__ """ Debuggable.__init__(self, name=self.name) self.Entries = {} self.core = core self.data = os.path.join(datastore, self.name) if not os.path.exists(self.data): self.logger.warning("%s: %s does not exist, creating" % (self.name, self.data)) os.makedirs(self.data) self.running = True @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.debug_log("Shutting down %s plugin" % self.name) self.running = False def set_debug(self, debug): for entry in self.Entries.values(): if isinstance(entry, Debuggable): entry.set_debug(debug) return Debuggable.set_debug(self, debug) def __str__(self): return "%s Plugin" % self.__class__.__name__
class Tool(object): """ The base tool class. All tools subclass this. .. private-include: _entry_is_complete .. autoattribute:: Bcfg2.Client.Tools.Tool.__execs__ .. autoattribute:: Bcfg2.Client.Tools.Tool.__handles__ .. autoattribute:: Bcfg2.Client.Tools.Tool.__req__ .. autoattribute:: Bcfg2.Client.Tools.Tool.__important__ """ options = [ Bcfg2.Options.Option( cf=('client', 'command_timeout'), help="Timeout when running external commands other than probes", type=Bcfg2.Options.Types.timeout)] #: The name of the tool. By default this uses #: :class:`Bcfg2.Client.Tools.ClassName` to ensure that it is the #: same as the name of the class. name = ClassName() #: Full paths to all executables the tool uses. When the tool is #: instantiated it will check to ensure that all of these files #: exist and are executable. __execs__ = [] #: A list of 2-tuples of entries handled by this tool. Each #: 2-tuple should contain ``(<tag>, <type>)``, where ``<type>`` is #: the ``type`` attribute of the entry. If this tool handles #: entries with no ``type`` attribute, specify None. __handles__ = [] #: A dict that describes the required attributes for entries #: handled by this tool. The keys are the names of tags. The #: values may either be lists of attribute names (if the same #: attributes are required by all tags of that name), or dicts #: whose keys are the ``type`` attribute and whose values are #: lists of attributes required by tags with that ``type`` #: attribute. In that case, the ``type`` attribute will also be #: required. __req__ = {} #: A list of entry names that will be treated as important and #: installed before other entries. __important__ = [] #: This tool is deprecated, and a warning will be produced if it #: is used. deprecated = False #: This tool is experimental, and a warning will be produced if it #: is used. experimental = False #: List of other tools (by name) that this tool conflicts with. #: If any of the listed tools are loaded, they will be removed at #: runtime with a warning. conflicts = [] def __init__(self, config): """ :param config: The XML configuration for this client :type config: lxml.etree._Element :raises: :exc:`Bcfg2.Client.Tools.ToolInstantiationError` """ #: A :class:`logging.Logger` object that will be used by this #: tool for logging self.logger = logging.getLogger(self.name) #: The XML configuration for this client self.config = config #: An :class:`Bcfg2.Utils.Executor` object for #: running external commands. self.cmd = Executor(timeout=Bcfg2.Options.setup.command_timeout) #: A list of entries that have been modified by this tool self.modified = [] #: A list of extra entries that are not listed in the #: configuration self.extra = [] #: A list of all entries handled by this tool self.handled = [] self._analyze_config() self._check_execs() def _analyze_config(self): """ Analyze the config at tool initialization-time for important and handled entries """ for struct in self.config: for entry in struct: if (entry.tag == 'Path' and entry.get('important', 'false').lower() == 'true'): self.__important__.append(entry.get('name')) self.handled = self.getSupportedEntries() def _check_execs(self): """ Check all executables used by this tool to ensure that they exist and are executable """ for filename in self.__execs__: try: mode = stat.S_IMODE(os.stat(filename)[stat.ST_MODE]) except OSError: raise ToolInstantiationError(sys.exc_info()[1]) except: raise ToolInstantiationError("%s: Failed to stat %s" % (self.name, filename)) if not mode & stat.S_IEXEC: raise ToolInstantiationError("%s: %s not executable" % (self.name, filename)) def BundleUpdated(self, bundle): # pylint: disable=W0613 """ Callback that is invoked when a bundle has been updated. :param bundle: The bundle that has been updated :type bundle: lxml.etree._Element :returns: dict - A dict of the state of entries suitable for updating :attr:`Bcfg2.Client.Client.states` """ return dict() def BundleNotUpdated(self, bundle): # pylint: disable=W0613 """ Callback that is invoked when a bundle has been updated. :param bundle: The bundle that has been updated :type bundle: lxml.etree._Element :returns: dict - A dict of the state of entries suitable for updating :attr:`Bcfg2.Client.Client.states` """ return dict() def Inventory(self, structures=None): """ Take an inventory of the system as it exists. This involves two steps: * Call the appropriate entry-specific Verify method for each entry this tool verifies; * Call :func:`Bcfg2.Client.Tools.Tool.FindExtra` to populate :attr:`Bcfg2.Client.Tools.Tool.extra` with extra entries. This implementation of :func:`Bcfg2.Client.Tools.Tool.Inventory` calls a ``Verify<tag>`` method to verify each entry, where ``<tag>`` is the entry tag. E.g., a Path entry would be verified by calling :func:`VerifyPath`. :param structures: The list of structures (i.e., bundles) to get entries from. If this is not given, all children of :attr:`Bcfg2.Client.Tools.Tool.config` will be used. :type structures: list of lxml.etree._Element :returns: dict - A dict of the state of entries suitable for updating :attr:`Bcfg2.Client.Client.states` """ if not structures: structures = self.config.getchildren() mods = self.buildModlist() states = dict() for struct in structures: for entry in struct.getchildren(): if self.canVerify(entry): try: func = getattr(self, "Verify%s" % entry.tag) except AttributeError: self.logger.error("%s: Cannot verify %s entries" % (self.name, entry.tag)) continue try: states[entry] = func(entry, mods) except: # pylint: disable=W0702 self.logger.error("%s: Unexpected failure verifying %s" % (self.name, self.primarykey(entry)), exc_info=1) self.extra = self.FindExtra() return states def Install(self, entries): """ Install entries. 'Install' in this sense means either initially install, or update as necessary to match the specification. This implementation of :func:`Bcfg2.Client.Tools.Tool.Install` calls a ``Install<tag>`` method to install each entry, where ``<tag>`` is the entry tag. E.g., a Path entry would be installed by calling :func:`InstallPath`. :param entries: The entries to install :type entries: list of lxml.etree._Element :returns: dict - A dict of the state of entries suitable for updating :attr:`Bcfg2.Client.Client.states` """ states = dict() for entry in entries: try: func = getattr(self, "Install%s" % entry.tag) except AttributeError: self.logger.error("%s: Cannot install %s entries" % (self.name, entry.tag)) continue try: states[entry] = func(entry) if states[entry]: self.modified.append(entry) except: # pylint: disable=W0702 self.logger.error("%s: Unexpected failure installing %s" % (self.name, self.primarykey(entry)), exc_info=1) return states def Remove(self, entries): """ Remove specified extra entries. :param entries: The entries to remove :type entries: list of lxml.etree._Element :returns: None """ pass def getSupportedEntries(self): """ Get all entries that are handled by this tool. :returns: list of lxml.etree._Element """ rv = [] for struct in self.config.getchildren(): rv.extend([entry for entry in struct.getchildren() if self.handlesEntry(entry)]) return rv def handlesEntry(self, entry): """ Return True if the entry is handled by this tool. :param entry: Determine if this entry is handled. :type entry: lxml.etree._Element :returns: bool """ return (entry.tag, entry.get('type')) in self.__handles__ def buildModlist(self): """ Build a list of all Path entries in the configuration. (This can be used to determine which paths might be modified from their original state, useful for verifying packages) :returns: list of lxml.etree._Element """ rv = [] for struct in self.config.getchildren(): rv.extend([entry.get('name') for entry in struct.getchildren() if entry.tag == 'Path']) return rv def missing_attrs(self, entry): """ Return a list of attributes that were expected on an entry (from :attr:`Bcfg2.Client.Tools.Tool.__req__`), but not found. :param entry: The entry to find missing attributes on :type entry: lxml.etree._Element :returns: list of strings """ required = self.__req__[entry.tag] if isinstance(required, dict): required = ["type"] try: required.extend(self.__req__[entry.tag][entry.get("type")]) except KeyError: pass return [attr for attr in required if attr not in entry.attrib or not entry.attrib[attr]] def canVerify(self, entry): """ Test if entry can be verified by calling :func:`Bcfg2.Client.Tools.Tool._entry_is_complete`. :param entry: The entry to evaluate :type entry: lxml.etree._Element :returns: bool - True if the entry can be verified, False otherwise. """ return self._entry_is_complete(entry, action="verify") def FindExtra(self): """ Return a list of extra entries, i.e., entries that exist on the client but are not in the configuration. :returns: list of lxml.etree._Element """ return [] def primarykey(self, entry): """ Return a string that describes the entry uniquely amongst all entries in the configuration. :param entry: The entry to describe :type entry: lxml.etree._Element :returns: string """ return "%s:%s" % (entry.tag, entry.get("name")) def canInstall(self, entry): """ Test if entry can be installed by calling :func:`Bcfg2.Client.Tools.Tool._entry_is_complete`. :param entry: The entry to evaluate :type entry: lxml.etree._Element :returns: bool - True if the entry can be installed, False otherwise. """ return self._entry_is_complete(entry, action="install") def _entry_is_complete(self, entry, action=None): """ Test if the entry is complete. This involves three things: * The entry is handled by this tool (as reported by :func:`Bcfg2.Client.Tools.Tool.handlesEntry`; * The entry does not report a bind failure; * The entry is not missing any attributes (as reported by :func:`Bcfg2.Client.Tools.Tool.missing_attrs`). :param entry: The entry to evaluate :type entry: lxml.etree._Element :param action: The action being performed on the entry (e.g., "install", "verify"). This is used to produce error messages; if not provided, generic error messages will be used. :type action: string :returns: bool - True if the entry can be verified, False otherwise. """ if not self.handlesEntry(entry): return False if 'failure' in entry.attrib: if action is None: msg = "%s: %s reports bind failure" else: msg = "%%s: Cannot %s entry %%s with bind failure" % action self.logger.error(msg % (self.name, self.primarykey(entry))) return False missing = self.missing_attrs(entry) if missing: if action is None: desc = "%s is" % self.primarykey(entry) else: desc = "Cannot %s %s due to" % (action, self.primarykey(entry)) self.logger.error("%s: %s missing required attribute(s): %s" % (self.name, desc, ", ".join(missing))) return False return True
class Plugin(Debuggable): """ The base class for all Bcfg2 Server plugins. """ #: The name of the plugin. name = ClassName() #: The email address of the plugin author. __author__ = '*****@*****.**' #: 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 #: Whether or not to automatically create a data directory for #: this plugin create = True #: List of names of methods to be exposed as XML-RPC functions __rmi__ = Debuggable.__rmi__ #: How exposed XML-RPC functions should be dispatched to child #: processes, if :mod:`Bcfg2.Server.MultiprocessingCore` is in #: use. Items ``__child_rmi__`` can either be strings (in which #: case the same function is called on child processes as on the #: parent) or 2-tuples, in which case the first element is the #: name of the RPC function called on the parent process, and the #: second element is the name of the function to call on child #: processes. Functions that are not listed in the list will not #: be dispatched to child processes, i.e., they will only be #: called on the parent. A function must be listed in ``__rmi__`` #: in order to be exposed; functions listed in ``_child_rmi__`` #: but not ``__rmi__`` will be ignored. __child_rmi__ = Debuggable.__child_rmi__ def __init__(self, core): """ :param core: The Bcfg2.Server.Core initializing the plugin :type core: Bcfg2.Server.Core :raises: :exc:`OSError` if adding a file monitor failed; :class:`Bcfg2.Server.Plugin.exceptions.PluginInitError` on other errors .. autoattribute:: Bcfg2.Server.Plugin.base.Debuggable.__rmi__ """ Debuggable.__init__(self, name=self.name) self.Entries = {} self.core = core self.data = os.path.join(Bcfg2.Options.setup.repository, self.name) if self.create and not os.path.exists(self.data): self.logger.warning("%s: %s does not exist, creating" % (self.name, self.data)) os.makedirs(self.data) self.running = True @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.debug_log("Shutting down %s plugin" % self.name) self.running = False def set_debug(self, debug): self.debug_log("%s: debug = %s" % (self.name, self.debug_flag), flag=True) for entry in self.Entries.values(): if isinstance(entry, Debuggable): entry.set_debug(debug) return Debuggable.set_debug(self, debug) def __str__(self): return "%s Plugin" % self.__class__.__name__