Exemple #1
0
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
Exemple #2
0
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__
Exemple #3
0
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
Exemple #4
0
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__