diff options
75 files changed, 1421 insertions, 582 deletions
diff --git a/debian/changelog b/debian/changelog index c41b2ecc2..59589de44 100644 --- a/debian/changelog +++ b/debian/changelog @@ -4,6 +4,12 @@ bcfg2 (1.4.0pre1-0.0) unstable; urgency=low -- Sol Jerome <sol.jerome@gmail.com> Mon, 16 Jun 2014 09:36:13 -0500 +bcfg2 (1.3.6-0.0) unstable; urgency=low + + * New upstream release + + -- Sol Jerome <sol.jerome@gmail.com> Thu, 11 Jun 2015 15:30:04 -0500 + bcfg2 (1.3.5-0.0) unstable; urgency=low * New upstream release diff --git a/doc/appendix/guides/authentication.txt b/doc/appendix/guides/authentication.txt index 93a34c9bc..eba01ee3c 100644 --- a/doc/appendix/guides/authentication.txt +++ b/doc/appendix/guides/authentication.txt @@ -145,7 +145,7 @@ Allowed values are: +-------------------+------------------------------------------+ ``cert+password`` is the default. This can be changed by setting the -``authentication`` parameter in the ``[communcation]`` section of +``authentication`` parameter in the ``[communication]`` section of ``bcfg2.conf``. For instance, to set ``bootstrap`` mode as the global default, you would add the following to ``bcfg2.conf``:: diff --git a/doc/client/tools/vcs.txt b/doc/client/tools/vcs.txt index fb9c33684..f21d097ff 100644 --- a/doc/client/tools/vcs.txt +++ b/doc/client/tools/vcs.txt @@ -8,8 +8,34 @@ VCS Client Tool .. warning: This tool is currently under development. -.. note: Currently, the only supported VCS is git. +.. note: Currently, the only supported VCS is git and svn. The VCS tool allows you to checkout particular revisions from a VCS repository on the client to a specified path. The tool requires the appropriate python libraries for the VCS used to be installed. + +See :ref:`server-plugins-generators-rules-vcs` for possible options. + +Example usage: + +You may want to create a `Rules/paths.xml` with the following: + +.. code-block:: xml + + <Rules priority="1"> + <Path name="/srv/bcfg2" type="vcs" + sourceurl="https://github.com/Bcfg2/bcfg2.git" + vcstype="git" + revision="cf6dfd8ca28e941b1e638ff0fa7e7a0a1ebb6a6f"/> + </Rules> + +Once the rule is created a client can reference the path from a +bundle, this path will then be populated from the repository. To +continue the above example, a file `Bundle/bcfg2.xml` might contain +this: + +.. code-block:: xml + + <Bundle name="bcfg"> + <Path name="/srv/bcfg2"> + <Bundle/> diff --git a/doc/development/compat.txt b/doc/development/compat.txt index 8700c46d3..132bf67c0 100644 --- a/doc/development/compat.txt +++ b/doc/development/compat.txt @@ -113,7 +113,7 @@ with Python 2.4 (and occasionally 2.5). Be sure to read the notes below, since some of these implementations may be feature-incomplete. +----------------+--------------------------------+--------------------------------------------+ -| Name | Python 2.4 | Python 2.4+ | +| Name | Python 2.4 | Python 2.5+ | +================+================================+============================================+ | formatdate | :func:`email.Utils.formatdate` | :func:`email.utils.formatdate` | +----------------+--------------------------------+--------------------------------------------+ @@ -129,6 +129,8 @@ below, since some of these implementations may be feature-incomplete. +----------------+--------------------------------+--------------------------------------------+ | MutableMapping | :class:`UserDict.DictMixin` | :class:`collections.MutableMapping` (2.6+) | +----------------+--------------------------------+--------------------------------------------+ +| literal_eval | :func:`eval` | :func:`ast.literal_eval`(2.6+) | ++----------------+--------------------------------+--------------------------------------------+ walk_packages ~~~~~~~~~~~~~ @@ -171,6 +173,14 @@ mind. :class:`collections.MutableMapping` is available in Python 2.6+, and will be used if available. +literal_eval +~~~~~~~~~~~~ + +:func:`ast.literal_eval` is a safe version of :func:`eval` that will only +allow delaration of literal strings, ints, list, dicts, etc. This was +introduced in Python 2.6, and as such Python 2.4 uses the plain-old +:func:`eval`. + Other Symbols ------------- diff --git a/doc/man/bcfg2.conf.txt b/doc/man/bcfg2.conf.txt index 62c4ac1a8..6c801ff1e 100644 --- a/doc/man/bcfg2.conf.txt +++ b/doc/man/bcfg2.conf.txt @@ -267,7 +267,7 @@ revision information out of your repository for reporting purposes. Ldap Plugin +++++++++++ -The Ldap plugin makes it possible to fetch data from an LDAP directory, +The Ldap plugin makes it possible to fetch data from a LDAP directory, process it and attach it to your metadata. Metadata Plugin @@ -718,6 +718,11 @@ Reporting options Maximum number of children for the reporting collector. Use 0 to disable the limit. (default is 0) + django_settings + Arbitrary options for the Django installation. The value expected + is a literal python dictionary, that is merged with the already set + django settings. + See Also -------- diff --git a/doc/releases/1.3.6.txt b/doc/releases/1.3.6.txt index 757fbf6f5..9ab024674 100644 --- a/doc/releases/1.3.6.txt +++ b/doc/releases/1.3.6.txt @@ -30,5 +30,14 @@ This is primarily a bugfix release. https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-OPTIONS +* SYSV: change instances of simplename to simplefile + + Previous configurations can be updated using the migration tool. + +* Authentication: Reject passwd auth, if authentication is set to "cert" +* Server/Core: drop privileges even if not running as daemon +* Packages/Yum.py: Fix dependency resolution logic +* Handle filesystem secontexts properly for contextless filesystems + Special thanks to the following contributors for this release: Michael -Fenn, Matt Kemp, Alexander Sulfrian, Jonathan Billings. +Fenn, Matt Kemp, Alexander Sulfrian, Jonathan Billings, Ross Smith. diff --git a/doc/releases/1.4.0pre2.txt b/doc/releases/1.4.0pre2.txt index a5c10777a..1ed2ae70f 100644 --- a/doc/releases/1.4.0pre2.txt +++ b/doc/releases/1.4.0pre2.txt @@ -19,6 +19,7 @@ environments. * NagiosGen: Add bundles to configuration * HomeBrew: Initial add of plugin +* Rules/Defaults: Add possibility to use name of entry in attributes backwards-incompatible user-facing changes ------------------------------------------ @@ -31,6 +32,20 @@ backwards-incompatible user-facing changes This fixes potentially long client runs when comparing files that have diverged significantly. +* The database options in the config (options and reporting_options in database + section) now have to be literal python dictionaries. + + This allows to set arbitrary options with nested settings. + +* The Ldap plugin changed significantly. The configuration interface was + simplified and new configuration options for the number of retries and the + delay in between were added. + + You have to register your ldap queries in the global list, there is no + distinction between LdapQueries and LdapSubQueries anymore, the names of + your queries default to the class names and the Ldap plugin expires + the metadata caches if the config file changes. + Thanks ------ diff --git a/doc/server/plugins/connectors/templatehelper.txt b/doc/server/plugins/connectors/templatehelper.txt index d113dcab7..e24ba10cb 100644 --- a/doc/server/plugins/connectors/templatehelper.txt +++ b/doc/server/plugins/connectors/templatehelper.txt @@ -54,9 +54,9 @@ See ``examples/TemplateHelper`` for examples of helper modules. Usage ===== -Specific helpers can be referred to in -templates as ``metadata.TemplateHelper[<modulename>]``. That accesses -a HelperModule object will have, as attributes, all symbols listed in +Specific helpers can be referred to in templates as +``metadata.TemplateHelper[<modulename>]``. That returns a HelperModule +object which will have, as attributes, all symbols listed in ``__export__``. For example, consider this helper module:: __export__ = ["hello"] diff --git a/doc/server/plugins/generators/cfg.txt b/doc/server/plugins/generators/cfg.txt index c991f20c9..7e7a7c72e 100644 --- a/doc/server/plugins/generators/cfg.txt +++ b/doc/server/plugins/generators/cfg.txt @@ -275,9 +275,7 @@ Several variables are pre-defined inside templates: | | <server-plugins-grouping-metadata-clientmetadata>` | +-------------+--------------------------------------------------------+ | name | The value of the ``name`` attribute as specified in | -| | the Path entry in Bcfg2. If an :ref:`altsrc | -| | <server-plugins-structures-altsrc>` attribute is used, | -| | then ``name`` will be the value of that attribute. | +| | the Path entry in Bcfg2. | +-------------+--------------------------------------------------------+ | source_path | The path to the template file on the filesystem | +-------------+--------------------------------------------------------+ diff --git a/doc/server/plugins/generators/nagiosgen.txt b/doc/server/plugins/generators/nagiosgen.txt index 1ccdd66c1..746adf44c 100644 --- a/doc/server/plugins/generators/nagiosgen.txt +++ b/doc/server/plugins/generators/nagiosgen.txt @@ -29,7 +29,6 @@ Create default host, and group specs in: check_period 24x7 contact_groups admins event_handler_enabled 1 - failure_prediction_enabled 1 flap_detection_enabled 1 initial_state o max_check_attempts 10 diff --git a/doc/server/plugins/generators/rules.txt b/doc/server/plugins/generators/rules.txt index 86478a5ae..7aeec6990 100644 --- a/doc/server/plugins/generators/rules.txt +++ b/doc/server/plugins/generators/rules.txt @@ -248,10 +248,13 @@ Manage symlinks. :onlyattrs: to :requiredattrs: to +.. _server-plugins-generators-rules-vcs: + vcs ^^^ -Check out the specified VCS repository to the given path. +Check out the specified VCS repository to the given path. See +:ref:`client-tools-vcs` for more details. .. xml:type:: PathType :nochildren: @@ -509,3 +512,23 @@ you'd have to explicitly specify ``<Service name="bcfg2.*".../>``. Note that only one Rule can apply to any abstract entry, so you cannot specify multiple regexes to match the same rule. + +Replacing the name of the Entry in Attributes +============================================= + +If you are using regular expressions to match the abstract configuration +entries, you may need the concrete name of the entry in some attributes. +To use this feature, you have to enable it. It is only useful, if used +together with regex matching. :: + + [rules] + regex = yes + replace_name = yes + +You now can write something like that in your xml file: + +.. code-block:: xml + + <POSIXUser name='.*' home='/somewhere/%{name}'/> + +``%{name}`` will be correctly replaced with the username for each POSIXUser. diff --git a/doc/server/plugins/grouping/ldap.txt b/doc/server/plugins/grouping/ldap.txt index 90590a272..96e224761 100644 --- a/doc/server/plugins/grouping/ldap.txt +++ b/doc/server/plugins/grouping/ldap.txt @@ -33,39 +33,38 @@ next section. Configuration ------------- -As processing LDAP search results can get pretty complex, the configuration has +As processing LDAP search results can get pretty complex, the configuration has to be written in Python. Here is a minimal example to get you started:: - from Bcfg2.Server.Plugins.Ldap import LdapConnection, LdapQuery, LdapSubQuery, register_query - - conn_default = LdapConnection() - conn_default.binddn = "uid=example,ou=People,dc=example,dc=com" - conn_default.bindpw = "foobat" - - @register_query + from Bcfg2.Server.Plugins.Ldap import LdapConnection, LdapQuery + + __queries__ = ['ExampleQuery'] + + conn_default = LdapConnection( + binddn="uid=example,ou=People,dc=example,dc=com", + bindpw = "foobat") + class ExampleQuery(LdapQuery): - name = "example" base = "ou=People,dc=example,dc=com" scope = "one" attrs = ["cn", "uid"] connection = conn_default - + def prepare_query(self, metadata): self.filter = "(personalServer=" + metadata.hostname + ")" - + def process_result(self, metadata): if not self.result: admin_uid = None admin_name = "This server has no admin." - return { + return { "admin_uid" : self.result[0][1]["uid"], "admin_name" : self.result[0][1]["cn"] } -The first line provides three classes for dealing with connections and queries -(details below) and a decorator function for registering your queries with the plugin. +The first line provides the two required classes for dealing with connections and queries. In this example our LDAP directory has a number of user objects in it. Each of those may have a personal server they administer. Whenever metadata for this machine is being @@ -73,7 +72,20 @@ generated by the Bcfg2 server, the UID and name of the admin are retrieved from In your bundles and config templates, you can access this data via the metadata object:: - ${metadata.Ldap["example"]["admin_name"]} + ${metadata.Ldap["ExampleQuery"]["admin_name"]} + +Connection retry +++++++++++++++++ + +If the LDAP server is down during a request, the LDAP plugin tries to reconnect after a +short delay. By default, it waits 3 seconds during the retries and tries to reconnect +up to three times. + +If you wish, you could customize these values in your ``bcfg2.conf``:: + + [ldap] + retries = 3 + retry_delay = 3.0 Class reference --------------- @@ -83,23 +95,23 @@ LdapConnection .. class:: LdapConnection - This class represents an LDAP connection. Every query must be associated with exactly + This class represents an LDAP connection. Every query must be associated with exactly one connection. - -.. attribute:: LdapConnection.binddn - + +.. attribute:: LdapConnection.binddn + DN used to authenticate against LDAP (required). - + .. attribute:: LdapConnection.bindpw - + Password for the previously mentioned **binddn** (required). - + .. attribute:: LdapConnection.host - + Hostname of host running the LDAP server (defaults to "localhost"). .. attribute:: LdapConnection.port - + Port where LDAP server is listening (defaults to 389). You may pass any of these attributes as keyword arguments when creating the connection object. @@ -108,143 +120,140 @@ LdapQuery +++++++++ .. class:: LdapQuery - + This class defines a single query that may adapt itself depending on the current metadata. .. attribute:: LdapQuery.attrs - + Can be used to retrieve only a certain subset of attributes. May either be a list of strings (attribute names) or ``None``, meaning all attributes (defaults to ``None``). .. attribute:: LdapQuery.base - - This is the search base. Only LDAP entries below this DN will be included in your + + This is the search base. Only LDAP entries below this DN will be included in your search results (required). - + .. attribute:: LdapQuery.connection - + Set this to an instance of the LdapConnection class (required). .. attribute:: LdapQuery.filter - + LDAP search filter used to narrow down search results (defaults to ``(objectClass=*)``). .. attribute:: LdapQuery.name - + This will be used as the dictionary key that provides access to the query results from - the metadata object (``metadata.Ldap["NAMEGOESHERE"]``) (required). + the metadata object: ``metadata.Ldap["NAMEGOESHERE"]`` (defaults to the class name). .. attribute:: LdapQuery.scope - - Set this to one of "base", "one" or "sub" to specify LDAP search depth (defaults to "sub"). + + Set this to one of "base", "one" or "sub" to specify LDAP search depth (defaults to "sub"). .. method:: LdapQuery.is_applicable(self, metadata) - + You can override this method to indicate whether this query makes sense for a given set of metadata (e.g. you need a query only for a certain bundle or group). - + (defaults to returning True) - -.. method:: LdapQuery.prepare_query(self, metadata) - + +.. method:: LdapQuery.prepare_query(self, metadata, \**kwargs) + Override this method to alter the query prior to execution. This is useful if your filter depends on the current metadata, e.g.:: - + self.filter = "(cn=" + metadata.hostname + ")" - + (defaults to doing nothing) -.. method:: LdapQuery.process_result(self, metadata) - +.. method:: LdapQuery.process_result(self, metadata, \**kwargs) + You will probably override this method in every query to reformat the results from LDAP. The raw result is stored in ``self.result``, you must return the altered data. Note that LDAP search results are presented in this structure:: - + ( ("DN of first entry returned", { "firstAttribute" : 1, "secondAttribute" : 2, - } + } ), ("DN of second entry returned", { "firstAttribute" : 1, "secondAttribute" : 2, - } + } ), ) - + Therefore, to return just the value of the firstAttribute of the second object returned, you'd write:: - + return self.result[1][1][0] - + (defaults to returning ``self.result`` unaltered) -LdapSubQuery -++++++++++++ - -.. class:: LdapSubQuery - - Sometimes you need more than one query to obtain the data you need (e.g. use the first - query to return all websites running on metadata.hostname and another query to find all - customers that should have access to those sites). - - LdapSubQueries are the same as LdapQueries, except for that the methods - - * ``get_result()`` - * ``prepare_query()`` - * ``process_result()`` - - allow any additional keyword arguments that may contain additional data as needed. Note - that ``get_result()`` will call ``prepare_query()`` and ``process_result()`` for you, - so you shouldn't ever need to invoke these yourself, just override them. - -Here is another example that uses LdapSubQuery:: - - class WebSitesQuery(LdapSubQuery): - name = "web_sites" +.. method:: LdapQuery.get_result(self, metadata, \**kwargs) + + This executes the query. First it will call ``prepare_query() for you, then it will try + to execute the query with the specified connection and last it will call ``process_result()`` + and return that return value. + +If you use a LdapQuery class by yourself, you could pass additional keyword arguments to +``get_result()``. It will call ``prepare_query()`` and ``process_result()`` for you and +also supply this additional arguments to this methods. + +Here is an example:: + + __queries__ = ['WebPackageQuery'] + + class WebSitesQuery(LdapQuery): filter = "(objectClass=webHostingSite)" attrs = ["dc"] connection = conn_default - + def prepare_query(self, metadata, base_dn): self.base = base_dn - - def process_result(self, metadata): + + def process_result(self, metadata, **kwargs): [...] # build sites dict from returned dc attributes return sites - - @register_query + class WebPackagesQuery(LdapQuery): - name = "web_packages" base = "dc=example,dc=com" attrs = ["customerId"] connection = conn_default - + def prepare_query(self, metadata): self.filter = "(&(objectClass=webHostingPackage)(cn:dn:=" + metadata.hostname + "))" - + def process_result(self, metadata): customers = {} for customer in self.result: dn = customer[0] cid = customer[1]["customerId"][0] - customers[cid]["sites"] = WebSitesQuery().get_result(metadata, base_dn = dn) + customers[cid]["sites"] = WebSitesQuery().get_result(metadata, base_dn=dn) return customers This example assumes that we have a number of webhosting packages that contain various -sites. We need a first query ("web_packages") to get a list of the packages our customers -have and another query for each of those to find out what sites are contained in each -package. The magic happens in the second class where ``WebSitesQuery.get_result()`` is -called with the additional ``base_dn`` parameter that allows our LdapSubQuery to only +sites. We need the ``WebPackagesQuery`` to get a list of the packages our customers +have and another query for each of those to find out what sites are contained in each +package. The magic happens in the second class where ``WebSitesQuery.get_result()`` is +called with the additional ``base_dn`` parameter that allows our LdapQuery to only search below that DN. -.. warning:: - Do NOT apply the ``register_query`` decorator to LdapSubQueries. +You do not need to add all LdapQueries to the ``__queries__`` list. Only add those to +that list, that should be called automatically and whose results should be added to the +client metadata. Known Issues ------------ * At this point there is no support for SSL/TLS. +* This module could not know, if a value changed on the LDAP server. So it could not + expire the client metadata cache sanely. + If you are using aggressive caching mode, this plugin will expire the metadata cache + for a single client at the start of a client run. If you are using LDAP data from + another client in a template, you will probably get the cached values from the last + client run of that other client. diff --git a/doc/server/plugins/structures/defaults.txt b/doc/server/plugins/structures/defaults.txt index 58b9feddb..9d37b8e64 100644 --- a/doc/server/plugins/structures/defaults.txt +++ b/doc/server/plugins/structures/defaults.txt @@ -29,3 +29,10 @@ on Fedora 15 and the ``chkconfig`` tool on Fedora 14, you could do:: If you were to specify a ``type`` attribute for a Service entry in Rules (or a ``type`` attribute for a BoundService entry in Bundler), that would take precendence over the default. + +Like :ref:`server-plugins-generators-rules`, Defaults can also replace +``%{name}`` in attributes with the real name of the entry. To enable this, +add the following setting to ``bcfg2.conf``:: + + [defaults] + replace_name = yes diff --git a/man/bcfg2.conf.5 b/man/bcfg2.conf.5 index 13000a719..43a28bad0 100644 --- a/man/bcfg2.conf.5 +++ b/man/bcfg2.conf.5 @@ -286,7 +286,7 @@ a Mercurial version control backend. Currently, it enables you to get revision information out of your repository for reporting purposes. .SS Ldap Plugin .sp -The Ldap plugin makes it possible to fetch data from an LDAP directory, +The Ldap plugin makes it possible to fetch data from a LDAP directory, process it and attach it to your metadata. .SS Metadata Plugin .sp @@ -778,6 +778,11 @@ Turn on Django debugging. .B max_children Maximum number of children for the reporting collector. Use 0 to disable the limit. (default is 0) +.TP +.B django_settings +Arbitrary options for the Django installation. The value expected +is a literal python dictionary, that is merged with the already set +django settings. .UNINDENT .UNINDENT .UNINDENT diff --git a/misc/bcfg2.spec b/misc/bcfg2.spec index 357afed4e..4b6da2441 100644 --- a/misc/bcfg2.spec +++ b/misc/bcfg2.spec @@ -18,11 +18,11 @@ # characters from the appropriate line below. # # Don't forget to change the Release: tag below to something like 0.1 -#%%global _rc 1 -%global _pre pre1 +#%%global _rc rc1 +#%%global _pre pre1 %global _nightly 1 %global _date %(date +%Y%m%d) -%global _pre_rc %{?_pre:pre%{_pre}}%{?_rc:rc%{_rc}} +%global _pre_rc %{?_pre:%{_pre}}%{?_rc:%{_rc}} # cherrypy 3.3 actually doesn't exist yet, but 3.2 has bugs that # prevent it from working: @@ -56,6 +56,8 @@ BuildRequires: python BuildRequires: python-devel BuildRequires: python-lxml BuildRequires: python-boto +BuildRequires: python-argparse +BuildRequires: python-jinja2 %if 0%{?suse_version} BuildRequires: python-M2Crypto BuildRequires: python-Genshi @@ -137,6 +139,8 @@ BuildRequires: systemd-units Requires: python-ssl %endif Requires: libselinux-python +Requires: pylibacl +Requires: python-argparse %if 0%{?fedora} >= 16 Requires(post): systemd-units @@ -192,6 +196,7 @@ Group: System Environment/Daemons %endif Requires: bcfg2 = %{version}-%{release} Requires: python-lxml >= 1.2.1 +Requires: python-genshi %if 0%{?suse_version} Requires: python-pyinotify Requires: python-python-daemon @@ -714,7 +719,9 @@ sed "s@http://www.w3.org/2001/xml.xsd@file://$(pwd)/schemas/xml.xsd@" \ %{python_sitelib}/Bcfg2/Server %{python_sitelib}/Bcfg2/Reporting %{python_sitelib}/Bcfg2/manage.py* +%if %{build_cherry_py} %exclude %{python_sitelib}/Bcfg2/Server/CherryPyCore.py* +%endif %dir %{_datadir}/bcfg2 %{_datadir}/bcfg2/schemas diff --git a/schemas/clients.xsd b/schemas/clients.xsd index 20a77b0dd..b6d04b3bf 100644 --- a/schemas/clients.xsd +++ b/schemas/clients.xsd @@ -60,7 +60,7 @@ <xsd:attribute type='xsd:string' name='profile' use='required'> <xsd:annotation> <xsd:documentation> - Profile group naem to associate this client with. + Profile group name to associate this client with. </xsd:documentation> </xsd:annotation> </xsd:attribute> diff --git a/src/lib/Bcfg2/Client/Tools/APT.py b/src/lib/Bcfg2/Client/Tools/APT.py index 5a86e8cd4..abc76ef1c 100644 --- a/src/lib/Bcfg2/Client/Tools/APT.py +++ b/src/lib/Bcfg2/Client/Tools/APT.py @@ -68,8 +68,8 @@ class APT(Bcfg2.Client.Tools.Tool): Bcfg2.Options.setup.apt_etc_path))] self.nonexistent = [entry.get('name') for struct in config for entry in struct - if entry.tag == 'Path' and - entry.get('type') == 'nonexistent'] + if (entry.tag == 'Path' and + entry.get('type') == 'nonexistent')] os.environ["DEBIAN_FRONTEND"] = 'noninteractive' self.actions = {} if Bcfg2.Options.setup.kevlar and not Bcfg2.Options.setup.dry_run: diff --git a/src/lib/Bcfg2/Client/Tools/FreeBSDInit.py b/src/lib/Bcfg2/Client/Tools/FreeBSDInit.py index 24bc4cf36..7c25e6804 100644 --- a/src/lib/Bcfg2/Client/Tools/FreeBSDInit.py +++ b/src/lib/Bcfg2/Client/Tools/FreeBSDInit.py @@ -42,7 +42,6 @@ class FreeBSDInit(Bcfg2.Client.Tools.SvcTool): self.logger.debug('Stopping service %s' % service.get('name')) return self.cmd.run(self.get_svc_command(service, 'onestop')) - def VerifyService(self, entry, _): """Verify Service status for entry.""" entry.set('target_status', entry.get('status')) # for reporting diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/File.py b/src/lib/Bcfg2/Client/Tools/POSIX/File.py index fc445e07c..1f1772d46 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIX/File.py +++ b/src/lib/Bcfg2/Client/Tools/POSIX/File.py @@ -8,6 +8,7 @@ import tempfile import Bcfg2.Options from Bcfg2.Client.Tools.POSIX.base import POSIXTool from Bcfg2.Compat import unicode, b64encode, b64decode # pylint: disable=W0622 +import Bcfg2.Utils class POSIXFile(POSIXTool): @@ -17,21 +18,6 @@ class POSIXFile(POSIXTool): def fully_specified(self, entry): return entry.text is not None or entry.get('empty', 'false') == 'true' - def _is_string(self, strng, encoding): - """ Returns true if the string contains no ASCII control - characters and can be decoded from the specified encoding. """ - for char in strng: - if ord(char) < 9 or ord(char) > 13 and ord(char) < 32: - return False - if not hasattr(strng, "decode"): - # py3k - return True - try: - strng.decode(encoding) - return True - except: # pylint: disable=W0702 - return False - def _get_data(self, entry): """ Get a tuple of (<file data>, <is binary>) for the given entry """ is_binary = entry.get('encoding', 'ascii') == 'base64' @@ -181,8 +167,8 @@ class POSIXFile(POSIXTool): (entry.get("name"), sys.exc_info()[1])) return False if not is_binary: - is_binary |= not self._is_string(content, - Bcfg2.Options.setup.encoding) + is_binary |= not Bcfg2.Utils.is_string( + content, Bcfg2.Options.setup.encoding) if is_binary: # don't compute diffs if the file is binary prompt.append('Binary file, no printable diff') diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/base.py b/src/lib/Bcfg2/Client/Tools/POSIX/base.py index 8895eaae1..488920989 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIX/base.py +++ b/src/lib/Bcfg2/Client/Tools/POSIX/base.py @@ -6,9 +6,11 @@ import pwd import grp import stat import copy +import errno import shutil import Bcfg2.Client.Tools import Bcfg2.Client.XML +import Bcfg2.Options from Bcfg2.Compat import oct_mode try: @@ -37,6 +39,22 @@ device_map = dict(block=stat.S_IFBLK, # pylint: disable=C0103 class POSIXTool(Bcfg2.Client.Tools.Tool): """ Base class for tools that handle POSIX (Path) entries """ + + options = [ + Bcfg2.Options.Option( + cf=('POSIX', 'secontext_ignore'), + default=['anon_inodefs_t', 'bdev_t', 'binfmt_misc_fs_t', + 'capifs_t', 'configfs_t', 'cpusetfs_t', 'ecryptfs_t', + 'eventpollfs_t', 'futexfs_t', 'hugetlbfs_t', 'ibmasmfs_t', + 'inotifyfs_t', 'mvfs_t', 'nfsd_fs_t', 'oprofilefs_t', + 'ramfs_t', 'romfs_t', 'rpc_pipefs_t', 'spufs_t', + 'squash_t', 'vmblock_t', 'vxfs_t', 'xenfs_t', 'autofs_t', + 'cifs_t', 'dosfs_t', 'fusefs_t', 'iso9660_t', + 'removable_t', 'nfs_t'], + help='secontext types to ignore labeling errors', + type=Bcfg2.Options.Types.colon_list) + ] + def fully_specified(self, entry): # pylint: disable=W0613 """ return True if the entry is fully specified """ # checking is done by __req__ @@ -272,7 +290,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): rv &= self._apply_acl(defacl, path, posix1e.ACL_TYPE_DEFAULT) return rv - def _set_secontext(self, entry, path=None): + def _set_secontext(self, entry, path=None): # pylint: disable=R0911 """ set the SELinux context of the file on disk according to the config""" if not HAS_SELINUX: @@ -284,25 +302,28 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): if not context: # no context listed return True - - if context == '__default__': - try: + secontext = selinux.lgetfilecon(path)[1].split(":")[2] + if secontext in Bcfg2.Options.setup.posix_secontext_ignore: + return True + try: + if context == '__default__': selinux.restorecon(path) - rv = True - except OSError: - err = sys.exc_info()[1] - self.logger.error("POSIX: Failed to restore SELinux context " - "for %s: %s" % (path, err)) - rv = False - else: - try: - rv = selinux.lsetfilecon(path, context) == 0 - except OSError: - err = sys.exc_info()[1] - self.logger.error("POSIX: Failed to restore SELinux context " - "for %s: %s" % (path, err)) - rv = False - return rv + return True + else: + return selinux.lsetfilecon(path, context) == 0 + except OSError: + err = sys.exc_info()[1] + if err.errno == errno.EOPNOTSUPP: + # Operation not supported + if context != '__default__': + self.logger.debug("POSIX: Failed to set SELinux context " + "for %s: %s" % (path, err)) + return False + return True + err = sys.exc_info()[1] + self.logger.error("POSIX: Failed to set or restore SELinux " + "context for %s: %s" % (path, err)) + return False def _norm_gid(self, gid): """ This takes a group name or gid and returns the diff --git a/src/lib/Bcfg2/Client/Tools/Systemd.py b/src/lib/Bcfg2/Client/Tools/Systemd.py index f7e5b1b0b..bfcc69475 100644 --- a/src/lib/Bcfg2/Client/Tools/Systemd.py +++ b/src/lib/Bcfg2/Client/Tools/Systemd.py @@ -84,7 +84,7 @@ class Systemd(Bcfg2.Client.Tools.SvcTool): else: if entry.get('status') == 'on': cmd = self.get_svc_command(entry, 'start') - else: + elif entry.get('status') == 'off': cmd = self.get_svc_command(entry, 'stop') if cmd: diff --git a/src/lib/Bcfg2/Client/__init__.py b/src/lib/Bcfg2/Client/__init__.py index 674162572..c40d66f58 100644 --- a/src/lib/Bcfg2/Client/__init__.py +++ b/src/lib/Bcfg2/Client/__init__.py @@ -929,6 +929,11 @@ class Client(object): 'Statistics', total=str(len(states)), version='2.0', revision=self.config.get('revision', '-1')) + flags = XML.SubElement(stats, "Flags") + XML.SubElement(flags, "Flag", name="dry_run", + value=str(Bcfg2.Options.setup.dry_run)) + XML.SubElement(flags, "Flag", name="only_important", + value=str(Bcfg2.Options.setup.only_important)) good_entries = [key for key, val in list(states.items()) if val] good = len(good_entries) stats.set('good', str(good)) diff --git a/src/lib/Bcfg2/Compat.py b/src/lib/Bcfg2/Compat.py index b8a75a0c5..1c2420ccf 100644 --- a/src/lib/Bcfg2/Compat.py +++ b/src/lib/Bcfg2/Compat.py @@ -286,3 +286,9 @@ except NameError: def cmp(a, b): """ Py3k implementation of cmp() """ return (a > b) - (a < b) + +# ast was introduced in python 2.6 +try: + from ast import literal_eval +except ImportError: + literal_eval = eval diff --git a/src/lib/Bcfg2/DBSettings.py b/src/lib/Bcfg2/DBSettings.py index 5a83c25c3..420f0ddd8 100644 --- a/src/lib/Bcfg2/DBSettings.py +++ b/src/lib/Bcfg2/DBSettings.py @@ -123,6 +123,9 @@ def finalize_django_config(opts=None, silent=False): opts.web_prefix.rstrip('/') + \ settings['MEDIA_URL'] + if opts.django_settings: + settings.update(opts.django_settings) + logger = logging.getLogger() logger.debug("Finalizing Django settings: %s" % settings) @@ -229,7 +232,7 @@ class _OptionContainer(object): dest='db_schema', default='public'), Bcfg2.Options.Option( cf=('database', 'options'), help='Database options', - dest='db_opts', type=Bcfg2.Options.Types.comma_dict, + dest='db_opts', type=Bcfg2.Options.Types.literal_dict, default=dict()), # reporting database options Bcfg2.Options.Option( @@ -258,20 +261,25 @@ class _OptionContainer(object): Bcfg2.Options.Option( cf=('database', 'reporting_options'), help='Reporting database options', dest='reporting_db_opts', - type=Bcfg2.Options.Types.comma_dict, default=dict()), + type=Bcfg2.Options.Types.literal_dict, default=dict()), # Django options Bcfg2.Options.Option( cf=('reporting', 'time_zone'), help='Django timezone'), Bcfg2.Options.BooleanOption( cf=('reporting', 'web_debug'), help='Django debug'), Bcfg2.Options.Option( - cf=('reporting', 'web_prefix'), help='Web prefix')] + cf=('reporting', 'web_prefix'), help='Web prefix'), + Bcfg2.Options.Option( + cf=('reporting', 'django_settings'), + help='Additional django settings', + type=Bcfg2.Options.Types.literal_dict, default=dict())] @staticmethod def component_parsed_hook(opts): """ Finalize the Django config after this component's options are parsed. """ - finalize_django_config(opts=opts) + if HAS_DJANGO: + finalize_django_config(opts=opts) @staticmethod def options_parsed_hook(): @@ -280,6 +288,7 @@ class _OptionContainer(object): early enough in option parsing to be parsed in the 'early' phase. Chances are good that things will break if that happens, but we do our best to be a good citizen. """ - finalize_django_config(silent=True) + if HAS_DJANGO: + finalize_django_config(silent=True) Bcfg2.Options.get_parser().add_component(_OptionContainer) diff --git a/src/lib/Bcfg2/Logger.py b/src/lib/Bcfg2/Logger.py index 11eaeebd1..e5f316a18 100644 --- a/src/lib/Bcfg2/Logger.py +++ b/src/lib/Bcfg2/Logger.py @@ -41,6 +41,8 @@ class TermiosFormatter(logging.Formatter): returns = [] line_len = self.width if isinstance(record.msg, str): + if len(record.args) != 0: + record.msg = record.msg % record.args for line in record.msg.split('\n'): if len(line) <= line_len: returns.append(line) diff --git a/src/lib/Bcfg2/Options/Parser.py b/src/lib/Bcfg2/Options/Parser.py index d146e3aa2..b72a495f1 100644 --- a/src/lib/Bcfg2/Options/Parser.py +++ b/src/lib/Bcfg2/Options/Parser.py @@ -141,6 +141,9 @@ class Parser(argparse.ArgumentParser): self.option_list.extend(option.list_options()) option.add_to_parser(self) + for opt in option.list_options(): + opt.default_from_config(self._cfp) + self._defaults_set.append(opt) def add_component(self, component): """ Add a component (and all of its options) to the diff --git a/src/lib/Bcfg2/Options/Types.py b/src/lib/Bcfg2/Options/Types.py index ac099e135..ad2e04f10 100644 --- a/src/lib/Bcfg2/Options/Types.py +++ b/src/lib/Bcfg2/Options/Types.py @@ -5,6 +5,7 @@ import os import re import pwd import grp +from Bcfg2.Compat import literal_eval _COMMA_SPLIT_RE = re.compile(r'\s*,\s*') @@ -32,28 +33,10 @@ def colon_list(value): return value.split(':') -def comma_dict(value): - """ Split an option string on commas, optionally surrounded by - whitespace, and split the resulting items again on equals signs, - returning a dict """ - result = dict() - if value: - items = comma_list(value) - for item in items: - if '=' in item: - key, value = item.split(r'=', 1) - if value in ["true", "yes", "on"]: - result[key] = True - elif value in ["false", "no", "off"]: - result[key] = False - else: - try: - result[key] = int(value) - except ValueError: - result[key] = value - else: - result[item] = True - return result +def literal_dict(value): + """ literally evaluate the option in order to allow for arbitrarily nested + dictionaries """ + return literal_eval(value) def anchored_regex_list(value): diff --git a/src/lib/Bcfg2/Reporting/Collector.py b/src/lib/Bcfg2/Reporting/Collector.py index 153809a35..f05a25732 100644 --- a/src/lib/Bcfg2/Reporting/Collector.py +++ b/src/lib/Bcfg2/Reporting/Collector.py @@ -116,7 +116,7 @@ class ReportingCollector(object): self.storage.__class__.__name__) self.storage.validate() except: - self.logger.error("Storage backed %s failed to validate: %s" % + self.logger.error("Storage backend %s failed to validate: %s" % (self.storage.__class__.__name__, sys.exc_info()[1])) diff --git a/src/lib/Bcfg2/Reporting/Reports.py b/src/lib/Bcfg2/Reporting/Reports.py index 3b9c83433..ebd0db58f 100755 --- a/src/lib/Bcfg2/Reporting/Reports.py +++ b/src/lib/Bcfg2/Reporting/Reports.py @@ -7,59 +7,40 @@ import datetime import Bcfg2.DBSettings -def hosts_by_entry_type(clients, etype, entryspec): - result = [] - for entry in entryspec: - for client in clients: - items = getattr(client.current_interaction, etype)() - for item in items: - if (item.entry_type == entry[0] and - item.name == entry[1]): - result.append(client) - return result - - -def print_fields(fields, client, fmt, extra=None): - """ Prints the fields specified in fields of client, max_name - specifies the column width of the name column. """ - fdata = [] - if extra is None: - extra = dict() - for field in fields: - if field == 'time': - fdata.append(str(client.current_interaction.timestamp)) - elif field == 'state': - if client.current_interaction.isclean(): - fdata.append("clean") - else: - fdata.append("dirty") - elif field == 'total': - fdata.append(client.current_interaction.total_count) - elif field == 'good': - fdata.append(client.current_interaction.good_count) - elif field == 'modified': - fdata.append(client.current_interaction.modified_count) - elif field == 'extra': - fdata.append(client.current_interaction.extra_count) - elif field == 'bad': - fdata.append((client.current_interaction.bad_count)) - elif field == 'stale': - fdata.append(client.current_interaction.isstale()) - else: - try: - fdata.append(getattr(client, field)) - except AttributeError: - fdata.append(extra.get(field, "N/A")) - - print(fmt % tuple(fdata)) - - def print_entries(interaction, etype): items = getattr(interaction, etype)() for item in items: print("%-70s %s" % (item.entry_type + ":" + item.name, etype)) +class _FlagsFilterMixin(object): + """ Mixin that allows to filter the interactions based on the + only_important and/or the dry_run flag """ + + options = [ + Bcfg2.Options.BooleanOption( + "-n", "--no-dry-run", + help="Do not consider interactions created with the --dry-run " + "flag"), + Bcfg2.Options.BooleanOption( + "-i", "--no-only-important", + help="Do not consider interactions created with the " + "--only-important flag")] + + def get_interaction(self, client, setup): + if not setup.no_dry_run and not setup.no_only_important: + return client.current_interaction + + filter = {} + if setup.no_dry_run: + filter['dry_run'] = False + if setup.no_only_important: + filter['only_important'] = False + + from Bcfg2.Reporting.models import Interaction + return Interaction.objects.filter(client=client, **filter).latest() + + class _SingleHostCmd(Bcfg2.Options.Subcommand): # pylint: disable=W0223 """ Base class for bcfg2-reports modes that take a single host as a positional argument """ @@ -74,10 +55,10 @@ class _SingleHostCmd(Bcfg2.Options.Subcommand): # pylint: disable=W0223 raise SystemExit(2) -class Show(_SingleHostCmd): +class Show(_SingleHostCmd, _FlagsFilterMixin): """ Show bad, extra, modified, or all entries from a given host """ - options = _SingleHostCmd.options + [ + options = _SingleHostCmd.options + _FlagsFilterMixin.options + [ Bcfg2.Options.BooleanOption( "-b", "--bad", help="Show bad entries from HOST"), Bcfg2.Options.BooleanOption( @@ -88,21 +69,25 @@ class Show(_SingleHostCmd): def run(self, setup): client = self.get_client(setup) show_all = not setup.bad and not setup.extra and not setup.modified + interaction = self.get_interaction(client, setup) if setup.bad or show_all: - print_entries(client.current_interaction, "bad") + print_entries(interaction, "bad") if setup.modified or show_all: - print_entries(client.current_interaction, "modified") + print_entries(interaction, "modified") if setup.extra or show_all: - print_entries(client.current_interaction, "extra") + print_entries(interaction, "extra") -class Total(_SingleHostCmd): +class Total(_SingleHostCmd, _FlagsFilterMixin): """ Show total number of managed and good entries from HOST """ + options = _SingleHostCmd.options + _FlagsFilterMixin.options + def run(self, setup): client = self.get_client(setup) - managed = client.current_interaction.total_count - good = client.current_interaction.good_count + interaction = self.get_interaction(client, setup) + managed = interaction.total_count + good = interaction.good_count print("Total managed entries: %d (good: %d)" % (managed, good)) @@ -120,9 +105,9 @@ class Expire(_SingleHostCmd): client.save() -class _ClientSelectCmd(Bcfg2.Options.Subcommand): +class _ClientSelectCmd(Bcfg2.Options.Subcommand, _FlagsFilterMixin): """ Base class for subcommands that display lists of clients """ - options = [ + options = _FlagsFilterMixin.options + [ Bcfg2.Options.Option("--fields", metavar="FIELD,FIELD,...", help="Only display the listed fields", type=Bcfg2.Options.Types.comma_list, @@ -132,7 +117,42 @@ class _ClientSelectCmd(Bcfg2.Options.Subcommand): from Bcfg2.Reporting.models import Client return Client.objects.exclude(current_interaction__isnull=True) - def display(self, result, fields, extra=None): + def _print_fields(self, setup, fields, client, fmt, extra=None): + """ Prints the fields specified in fields of client, max_name + specifies the column width of the name column. """ + fdata = [] + if extra is None: + extra = dict() + interaction = self.get_interaction(client, setup) + for field in fields: + if field == 'time': + fdata.append(str(interaction.timestamp)) + elif field == 'state': + if interaction.isclean(): + fdata.append("clean") + else: + fdata.append("dirty") + elif field == 'total': + fdata.append(interaction.total_count) + elif field == 'good': + fdata.append(interaction.good_count) + elif field == 'modified': + fdata.append(interaction.modified_count) + elif field == 'extra': + fdata.append(interaction.extra_count) + elif field == 'bad': + fdata.append(interaction.bad_count) + elif field == 'stale': + fdata.append(interaction.isstale()) + else: + try: + fdata.append(getattr(client, field)) + except AttributeError: + fdata.append(extra.get(field, "N/A")) + + print(fmt % tuple(fdata)) + + def display(self, setup, result, fields, extra=None): if 'name' not in fields: fields.insert(0, "name") if not result: @@ -153,8 +173,8 @@ class _ClientSelectCmd(Bcfg2.Options.Subcommand): print(fmt % tuple(f.title() for f in fields)) for client in result: if not client.expiration: - print_fields(fields, client, fmt, - extra=extra.get(client, None)) + self._print_fields(setup, fields, client, fmt, + extra=extra.get(client, None)) class Clients(_ClientSelectCmd): @@ -172,14 +192,14 @@ class Clients(_ClientSelectCmd): result = [] show_all = not setup.stale and not setup.clean and not setup.dirty for client in self.get_clients(): - interaction = client.current_interaction + interaction = self.get_interaction(client, setup) if (show_all or (setup.stale and interaction.isstale()) or (setup.clean and interaction.isclean()) or (setup.dirty and not interaction.isclean())): result.append(client) - self.display(result, setup.fields) + self.display(setup, result, setup.fields) class Entries(_ClientSelectCmd): @@ -201,6 +221,18 @@ class Entries(_ClientSelectCmd): Bcfg2.Options.PositionalArgument( "entries", metavar="TYPE:NAME", nargs="*")] + def _hosts_by_entry_type(self, setup, clients, etype, entryspec): + result = [] + for entry in entryspec: + for client in clients: + interaction = self.get_interaction(client, setup) + items = getattr(interaction, etype)() + for item in items: + if (item.entry_type == entry[0] and + item.name == entry[1]): + result.append(client) + return result + def run(self, setup): result = [] if setup.file: @@ -216,13 +248,15 @@ class Entries(_ClientSelectCmd): clients = self.get_clients() if setup.badentry: - result = hosts_by_entry_type(clients, "bad", entries) + result = self._hosts_by_entry_type(setup, clients, "bad", entries) elif setup.modifiedentry: - result = hosts_by_entry_type(clients, "modified", entries) + result = self._hosts_by_entry_type(setup, clients, "modified", + entries) elif setup.extraentry: - result = hosts_by_entry_type(clients, "extra", entries) + result = self._hosts_by_entry_type(setup, clients, "extra", + entries) - self.display(result, setup.fields) + self.display(setup, result, setup.fields) class Entry(_ClientSelectCmd): @@ -252,14 +286,14 @@ class Entry(_ClientSelectCmd): for client in self.get_clients(): ents = entry_cls.objects.filter( name=ename, - interaction=client.current_interaction) + interaction=self.get_interaction(client, setup)) if len(ents) == 0: continue extra[client] = {"entry state": ents[0].get_state_display(), "reason": ents[0]} result.append(client) - self.display(result, fields, extra=extra) + self.display(setup, result, fields, extra=extra) class CLI(Bcfg2.Options.CommandRegistry): diff --git a/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py b/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py index efd9e594c..c9aa169bf 100644 --- a/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py +++ b/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py @@ -284,6 +284,14 @@ class DjangoORM(StorageBase): Group.objects.get_or_create(name=metadata['profile']) else: profile = None + + flags = {'dry_run': False, 'only_important': False} + for flag in stats.findall('./Flags/Flag'): + value = flag.get('value', default='false').lower() == 'true' + name = flag.get('name') + if name in flags: + flags[name] = value + inter = Interaction(client=client, timestamp=timestamp, state=stats.get('state', default="unknown"), @@ -292,7 +300,8 @@ class DjangoORM(StorageBase): good_count=stats.get('good', default="0"), total_count=stats.get('total', default="0"), server=server, - profile=profile) + profile=profile, + **flags) inter.save() self.logger.debug("Interaction for %s at %s with INSERTED in to db" % (client.id, timestamp)) diff --git a/src/lib/Bcfg2/Reporting/migrations/0007_add_flag_fields_interaction.py b/src/lib/Bcfg2/Reporting/migrations/0007_add_flag_fields_interaction.py new file mode 100644 index 000000000..491ecb845 --- /dev/null +++ b/src/lib/Bcfg2/Reporting/migrations/0007_add_flag_fields_interaction.py @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Interaction.dry_run' + db.add_column('Reporting_interaction', 'dry_run', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + # Adding field 'Interaction.only_important' + db.add_column('Reporting_interaction', 'only_important', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Interaction.dry_run' + db.delete_column('Reporting_interaction', 'dry_run') + + # Deleting field 'Interaction.only_important' + db.delete_column('Reporting_interaction', 'only_important') + + + models = { + 'Reporting.actionentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'ActionEntry'}, + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'output': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'check'", 'max_length': '128'}) + }, + 'Reporting.bundle': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Bundle'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'Reporting.client': { + 'Meta': {'object_name': 'Client'}, + 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'current_interaction': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'parent_client'", 'null': 'True', 'to': "orm['Reporting.Interaction']"}), + 'expiration': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'Reporting.deviceentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'DeviceEntry', '_ormbases': ['Reporting.PathEntry']}, + 'current_major': ('django.db.models.fields.IntegerField', [], {}), + 'current_minor': ('django.db.models.fields.IntegerField', [], {}), + 'device_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'pathentry_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['Reporting.PathEntry']", 'unique': 'True', 'primary_key': 'True'}), + 'target_major': ('django.db.models.fields.IntegerField', [], {}), + 'target_minor': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.failureentry': { + 'Meta': {'object_name': 'FailureEntry'}, + 'entry_type': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'Reporting.fileacl': { + 'Meta': {'object_name': 'FileAcl'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'Reporting.fileperms': { + 'Meta': {'unique_together': "(('owner', 'group', 'mode'),)", 'object_name': 'FilePerms'}, + 'group': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'owner': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'Reporting.group': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Group'}, + 'bundles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Bundle']", 'symmetrical': 'False'}), + 'category': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Group']", 'symmetrical': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'profile': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'Reporting.interaction': { + 'Meta': {'ordering': "['-timestamp']", 'unique_together': "(('client', 'timestamp'),)", 'object_name': 'Interaction'}, + 'actions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.ActionEntry']", 'symmetrical': 'False'}), + 'bad_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'bundles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Bundle']", 'symmetrical': 'False'}), + 'client': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'interactions'", 'to': "orm['Reporting.Client']"}), + 'dry_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'extra_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'failures': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.FailureEntry']", 'symmetrical': 'False'}), + 'good_count': ('django.db.models.fields.IntegerField', [], {}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Group']", 'symmetrical': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'only_important': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'packages': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.PackageEntry']", 'symmetrical': 'False'}), + 'paths': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.PathEntry']", 'symmetrical': 'False'}), + 'posixgroups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.POSIXGroupEntry']", 'symmetrical': 'False'}), + 'posixusers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.POSIXUserEntry']", 'symmetrical': 'False'}), + 'profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['Reporting.Group']"}), + 'repo_rev_code': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'sebooleans': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEBooleanEntry']", 'symmetrical': 'False'}), + 'sefcontexts': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEFcontextEntry']", 'symmetrical': 'False'}), + 'seinterfaces': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEInterfaceEntry']", 'symmetrical': 'False'}), + 'selogins': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SELoginEntry']", 'symmetrical': 'False'}), + 'semodules': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEModuleEntry']", 'symmetrical': 'False'}), + 'senodes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SENodeEntry']", 'symmetrical': 'False'}), + 'sepermissives': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEPermissiveEntry']", 'symmetrical': 'False'}), + 'seports': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEPortEntry']", 'symmetrical': 'False'}), + 'server': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'services': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.ServiceEntry']", 'symmetrical': 'False'}), + 'seusers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEUserEntry']", 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'total_count': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.linkentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'LinkEntry', '_ormbases': ['Reporting.PathEntry']}, + 'current_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'pathentry_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['Reporting.PathEntry']", 'unique': 'True', 'primary_key': 'True'}), + 'target_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}) + }, + 'Reporting.packageentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'PackageEntry'}, + 'current_version': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'target_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'verification_details': ('django.db.models.fields.TextField', [], {'default': "''"}) + }, + 'Reporting.pathentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'PathEntry'}, + 'acls': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.FileAcl']", 'symmetrical': 'False'}), + 'current_perms': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['Reporting.FilePerms']"}), + 'detail_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'details': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'path_type': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'target_perms': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['Reporting.FilePerms']"}) + }, + 'Reporting.performance': { + 'Meta': {'object_name': 'Performance'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'interaction': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'performance_items'", 'to': "orm['Reporting.Interaction']"}), + 'metric': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'value': ('django.db.models.fields.DecimalField', [], {'max_digits': '32', 'decimal_places': '16'}) + }, + 'Reporting.posixgroupentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'POSIXGroupEntry'}, + 'current_gid': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'gid': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.posixuserentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'POSIXUserEntry'}, + 'current_gecos': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'current_group': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'current_home': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'current_shell': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'current_uid': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'gecos': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'group': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'home': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'shell': ('django.db.models.fields.CharField', [], {'default': "'/bin/bash'", 'max_length': '1024'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'uid': ('django.db.models.fields.IntegerField', [], {'null': 'True'}) + }, + 'Reporting.sebooleanentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEBooleanEntry'}, + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'value': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'Reporting.sefcontextentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEFcontextEntry'}, + 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filetype': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.seinterfaceentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEInterfaceEntry'}, + 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.seloginentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SELoginEntry'}, + 'current_selinuxuser': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'selinuxuser': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.semoduleentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEModuleEntry'}, + 'current_disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.senodeentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SENodeEntry'}, + 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'proto': ('django.db.models.fields.CharField', [], {'max_length': '4'}), + 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.sepermissiveentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEPermissiveEntry'}, + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.seportentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEPortEntry'}, + 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.serviceentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'ServiceEntry'}, + 'current_status': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'target_status': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}) + }, + 'Reporting.seuserentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEUserEntry'}, + 'current_prefix': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'current_roles': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'prefix': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'roles': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['Reporting']
\ No newline at end of file diff --git a/src/lib/Bcfg2/Reporting/models.py b/src/lib/Bcfg2/Reporting/models.py index ae6f6731b..8e2c644fb 100644 --- a/src/lib/Bcfg2/Reporting/models.py +++ b/src/lib/Bcfg2/Reporting/models.py @@ -144,6 +144,8 @@ class Interaction(models.Model): bad_count = models.IntegerField(default=0) modified_count = models.IntegerField(default=0) extra_count = models.IntegerField(default=0) + dry_run = models.BooleanField(default=False) + only_important = models.BooleanField(default=False) actions = models.ManyToManyField("ActionEntry") packages = models.ManyToManyField("PackageEntry") @@ -630,7 +632,7 @@ class POSIXGroupEntry(SuccessEntry): class PackageEntry(SuccessEntry): """ The new model for package information """ - # if this is an extra entry trget_version will be empty + # if this is an extra entry target_version will be empty target_version = models.CharField(max_length=1024, default='') current_version = models.CharField(max_length=1024) verification_details = models.TextField(default="") diff --git a/src/lib/Bcfg2/Reporting/templates/base-timeview.html b/src/lib/Bcfg2/Reporting/templates/base-timeview.html index 9a5ef651c..28a9fa0f4 100644 --- a/src/lib/Bcfg2/Reporting/templates/base-timeview.html +++ b/src/lib/Bcfg2/Reporting/templates/base-timeview.html @@ -17,7 +17,7 @@ function bcfg2_check_date() { } document.write(getCalendarStyles()); </script> -{% if not timestamp %}Rendered at {% now "Y-m-d H:i" %} | {% else %}View as of {{ timestamp|date:"Y-m-d H:i" }} | {% endif %}{% spaceless %} +{% if not timestamp %}Rendered at {% now "SHORT_DATETIME_FORMAT" %} | {% else %}View as of {{ timestamp|date:"SHORT_DATETIME_FORMAT" }} | {% endif %}{% spaceless %} <a id='cal_link' name='cal_link' href='#' onclick='showCalendar(); return false;' >[change]</a> <form method='post' action='{{ path }}' id='cal_form' name='cal_form'> diff --git a/src/lib/Bcfg2/Reporting/templates/clients/detail.html b/src/lib/Bcfg2/Reporting/templates/clients/detail.html index e890589a7..6809dcc2d 100644 --- a/src/lib/Bcfg2/Reporting/templates/clients/detail.html +++ b/src/lib/Bcfg2/Reporting/templates/clients/detail.html @@ -39,7 +39,7 @@ span.history_links a { <select id="quick" name="quick" onchange="javascript:pageJump('quick');"> <option value="" selected="selected">--- Time ---</option> {% for i in client.interactions.all|slice:":25" %} - <option value="{% url "reports_client_detail_pk" hostname=client.name pk=i.id %}">{{i.timestamp|date:"c"}}</option> + <option value="{% url "reports_client_detail_pk" hostname=client.name pk=i.id %}">{{i.timestamp|date:"DATETIME_FORMAT"}}</option> {% endfor %} </select></span> </div> @@ -90,7 +90,7 @@ span.history_links a { <div class='entry_list'> <div class='entry_list_head' onclick='javascript:toggleMe("bundles_table");'> <h3>Bundle membership</h3> - <div class='entry_expand_tab' id='plusminus_bundless_table'>[+]</div> + <div class='entry_expand_tab' id='plusminus_bundles_table'>[+]</div> </div> <table id='bundles_table' class='entry_list' style='display: none'> {% endif %} @@ -127,7 +127,7 @@ span.history_links a { <div class='entry_list'> <div class='entry_list_head failed-lineitem' onclick='javascript:toggleMe("failures_table");'> <h3>Failed Entries — {{ interaction.failures.all|length }}</h3> - <div class='entry_expand_tab' id='plusminus_failuress_table'>[+]</div> + <div class='entry_expand_tab' id='plusminus_failures_table'>[+]</div> </div> <table id='failures_table' class='entry_list' style='display: none'> {% for failure in interaction.failures.all %} diff --git a/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html b/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html index 6a314bd88..cf778504a 100644 --- a/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html +++ b/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html @@ -32,7 +32,9 @@ This is needed for Django versions less than 1.5 <td class='right_column_narrow'>{{ entry.bad_count }}</td> <td class='right_column_narrow'>{{ entry.modified_count }}</td> <td class='right_column_narrow'>{{ entry.extra_count }}</td> - <td class='right_column'><span {% if entry.isstale %}class='dirty-lineitem'{% endif %}>{{ entry.timestamp|date:"Y-m-d\&\n\b\s\p\;H:i"|safe }}</span></td> + <td class='right_column'><span {% if entry.isstale %}class='dirty-lineitem'{% endif %} style="white-space: nowrap;"> + {{ entry.timestamp|date:"SHORT_DATETIME_FORMAT"|safe }} + </span></td> <td class='right_column_wide'> {% if entry.server %} <a href='{% add_url_filter server=entry.server %}'>{{ entry.server }}</a> diff --git a/src/lib/Bcfg2/Reporting/templates/config_items/entry_status.html b/src/lib/Bcfg2/Reporting/templates/config_items/entry_status.html index e3befb0eb..8a5d93690 100644 --- a/src/lib/Bcfg2/Reporting/templates/config_items/entry_status.html +++ b/src/lib/Bcfg2/Reporting/templates/config_items/entry_status.html @@ -22,7 +22,9 @@ This is needed for Django versions less than 1.5 {% for inter in inters %} <tr class='{% cycle listview,listview_alt %}'> <td><a href='{% url "reports_client_detail" hostname=inter.client.name %}'>{{inter.client.name}}</a></td> - <td><a href='{% url "reports_client_detail_pk" hostname=inter.client.name pk=inter.pk %}'>{{inter.timestamp|date:"Y-m-d\&\n\b\s\p\;H:i"|safe}}</a></td> + <td><a href='{% url "reports_client_detail_pk" hostname=inter.client.name pk=inter.pk %}' style="white-space: nowrap;"> + {{inter.timestamp|date:"SHORT_DATETIME_FORMAT"|safe}} + </a></td> <td>{{ item.get_state_display }}</td> <td style='white-space: nowrap'><a href='{% url "reports_item" entry_type=item.class_name pk=item.pk %}'>({{item.pk}}) {{item.short_list|join:","}}</a></td> </tr> diff --git a/src/lib/Bcfg2/Reporting/templates/config_items/item.html b/src/lib/Bcfg2/Reporting/templates/config_items/item.html index c6e6df020..2e2fd36fa 100644 --- a/src/lib/Bcfg2/Reporting/templates/config_items/item.html +++ b/src/lib/Bcfg2/Reporting/templates/config_items/item.html @@ -130,7 +130,7 @@ div.entry_list h3 { <div class='entry_list'> <div class='entry_list_head'> - <h3>Occurences on {{ timestamp|date:"Y-m-d" }}</h3> + <h3>Occurences on {{ timestamp|date:"SHORT_DATE_FORMAT" }}</h3> </div> {% if associated_list %} <table class="entry_list" cellpadding="3"> diff --git a/src/lib/Bcfg2/Reporting/templates/widgets/interaction_list.inc b/src/lib/Bcfg2/Reporting/templates/widgets/interaction_list.inc index 30ed2fd3e..444d05e1e 100644 --- a/src/lib/Bcfg2/Reporting/templates/widgets/interaction_list.inc +++ b/src/lib/Bcfg2/Reporting/templates/widgets/interaction_list.inc @@ -15,7 +15,9 @@ </tr> {% for entry in entry_list %} <tr class='{% cycle listview,listview_alt %}'> - <td class='left_column'><a href='{% url reports_client_detail_pk hostname=entry.client.name, pk=entry.id %}'>{{ entry.timestamp|date:"Y-m-d\&\n\b\s\p\;H:i"|safe }}</a></td> + <td class='left_column'><a href='{% url reports_client_detail_pk hostname=entry.client.name, pk=entry.id %}' class="white-space: nowrap;"> + {{ entry.timestamp|date:"SHORT_DATETIME_FORMAT"|safe }} + </a></td> {% if not client %} <td class='right_column_wide'><a href='{% add_url_filter hostname=entry.client.name %}'>{{ entry.client.name }}</a></td> {% endif %} diff --git a/src/lib/Bcfg2/Server/BuiltinCore.py b/src/lib/Bcfg2/Server/BuiltinCore.py index e138c57e4..dc5cc46fb 100644 --- a/src/lib/Bcfg2/Server/BuiltinCore.py +++ b/src/lib/Bcfg2/Server/BuiltinCore.py @@ -34,7 +34,8 @@ class BuiltinCore(NetworkCore): daemon_args = dict(uid=Bcfg2.Options.setup.daemon_uid, gid=Bcfg2.Options.setup.daemon_gid, umask=int(Bcfg2.Options.setup.umask, 8), - detach_process=True) + detach_process=True, + files_preserve=self._logfilehandles()) if Bcfg2.Options.setup.daemon: daemon_args['pidfile'] = TimeoutPIDLockFile( Bcfg2.Options.setup.daemon, acquire_timeout=5) @@ -44,6 +45,24 @@ class BuiltinCore(NetworkCore): self.context = daemon.DaemonContext(**daemon_args) __init__.__doc__ = NetworkCore.__init__.__doc__.split('.. -----')[0] + def _logfilehandles(self, logger=None): + """ Get a list of all filehandles logger, that have to be handled + with DaemonContext.files_preserve to keep looging working. + + :param logger: The logger to get the file handles of. By default, + self.logger is used. + :type logger: logging.Logger + """ + if logger is None: + logger = self.logger + + handles = [handler.stream.fileno() + for handler in logger.handlers + if hasattr(handler, 'stream')] + if logger.parent: + handles += self._logfilehandles(logger.parent) + return handles + def _dispatch(self, method, args, dispatch_dict): """ Dispatch XML-RPC method calls diff --git a/src/lib/Bcfg2/Server/CherrypyCore.py b/src/lib/Bcfg2/Server/CherrypyCore.py index 3cb0e291b..05c6c5a94 100644 --- a/src/lib/Bcfg2/Server/CherrypyCore.py +++ b/src/lib/Bcfg2/Server/CherrypyCore.py @@ -110,17 +110,21 @@ class CherrypyCore(NetworkCore): return cherrypy.serving.response.body def _daemonize(self): - """ Drop privileges with - :class:`cherrypy.process.plugins.DropPrivileges`, daemonize - with :class:`cherrypy.process.plugins.Daemonizer`, and write a + """ Drop privileges, daemonize + with :class:`cherrypy.process.plugins.Daemonizer` and write a PID file with :class:`cherrypy.process.plugins.PIDFile`. """ + self._drop_privileges() + Daemonizer(cherrypy.engine).subscribe() + PIDFile(cherrypy.engine, Bcfg2.Options.setup.daemon).subscribe() + return True + + def _drop_privileges(self): + """ Drop privileges with + :class:`cherrypy.process.plugins.DropPrivileges` """ DropPrivileges(cherrypy.engine, uid=Bcfg2.Options.setup.daemon_uid, gid=Bcfg2.Options.setup.daemon_gid, umask=int(Bcfg2.Options.setup.umask, 8)).subscribe() - Daemonizer(cherrypy.engine).subscribe() - PIDFile(cherrypy.engine, Bcfg2.Options.setup.daemon).subscribe() - return True def _run(self): """ Start the server listening. """ diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py index 03ab40343..25acc8ac0 100644 --- a/src/lib/Bcfg2/Server/Core.py +++ b/src/lib/Bcfg2/Server/Core.py @@ -11,6 +11,7 @@ import threading import time import inspect import lxml.etree +import daemon import Bcfg2.Server import Bcfg2.Logger import Bcfg2.Options @@ -681,7 +682,7 @@ class Core(object): self.logger.debug("Building configuration for %s" % client) start = time.time() config = lxml.etree.Element("Configuration", version='2.0', - revision=self.revision) + revision=str(self.revision)) try: meta = self.build_metadata(client) except MetadataConsistencyError: @@ -1486,3 +1487,13 @@ class NetworkCore(Core): """ Daemonize the server and write the pidfile. This must be overridden by a core implementation. """ raise NotImplementedError + + def _drop_privileges(self): + """ This is called if not daemonized and running as root to + drop the privileges to the configured daemon_uid and daemon_gid. + """ + daemon.daemon.change_process_owner( + Bcfg2.Options.setup.daemon_uid, + Bcfg2.Options.setup.daemon_gid) + self.logger.debug("Dropped privileges to %s:%s." % + (os.getuid(), os.getgid())) diff --git a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py index c4b34a469..8f6e136fd 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py +++ b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py @@ -214,6 +214,7 @@ class Inotify(Pseudo, pyinotify.ProcessEvent): def shutdown(self): if self.started and self.notifier: self.notifier.stop() + Pseudo.shutdown(self) shutdown.__doc__ = Pseudo.shutdown.__doc__ def list_watches(self): diff --git a/src/lib/Bcfg2/Server/Lint/MergeFiles.py b/src/lib/Bcfg2/Server/Lint/MergeFiles.py index 8e6a926ae..3a6251594 100644 --- a/src/lib/Bcfg2/Server/Lint/MergeFiles.py +++ b/src/lib/Bcfg2/Server/Lint/MergeFiles.py @@ -6,6 +6,7 @@ import copy from difflib import SequenceMatcher import Bcfg2.Server.Lint from Bcfg2.Server.Plugins.Cfg import CfgGenerator +from Bcfg2.Utils import is_string def threshold(val): @@ -50,6 +51,8 @@ class MergeFiles(Bcfg2.Server.Lint.ServerPlugin): for filename, entryset in self.core.plugins['Cfg'].entries.items(): candidates = dict([(f, e) for f, e in entryset.entries.items() if (isinstance(e, CfgGenerator) and + is_string(e.data, + Bcfg2.Options.setup.encoding) and f not in ignore and not f.endswith(".crypt"))]) similar, identical = self.get_similar(candidates) diff --git a/src/lib/Bcfg2/Server/Lint/TemplateHelper.py b/src/lib/Bcfg2/Server/Lint/TemplateHelper.py index 9d05516f1..ce6fdca74 100644 --- a/src/lib/Bcfg2/Server/Lint/TemplateHelper.py +++ b/src/lib/Bcfg2/Server/Lint/TemplateHelper.py @@ -4,8 +4,8 @@ import sys import imp from Bcfg2.Server.Lint import ServerPlugin -from Bcfg2.Server.Plugins.TemplateHelper import HelperModule, MODULE_RE, \ - safe_module_name +from Bcfg2.Server.Plugins.TemplateHelper import HelperModule, MODULE_RE +from Bcfg2.Utils import safe_module_name class TemplateHelper(ServerPlugin): @@ -44,7 +44,8 @@ class TemplateHelper(ServerPlugin): module_name = MODULE_RE.search(helper).group(1) try: - module = imp.load_source(safe_module_name(module_name), helper) + module = imp.load_source( + safe_module_name('TemplateHelper', module_name), helper) except: # pylint: disable=W0702 err = sys.exc_info()[1] self.LintError("templatehelper-import-error", diff --git a/src/lib/Bcfg2/Server/Lint/__init__.py b/src/lib/Bcfg2/Server/Lint/__init__.py index 61f704206..873e5f149 100644 --- a/src/lib/Bcfg2/Server/Lint/__init__.py +++ b/src/lib/Bcfg2/Server/Lint/__init__.py @@ -14,6 +14,7 @@ import time import lxml.etree + import Bcfg2.Options import Bcfg2.Server.Core import Bcfg2.Server.Plugins diff --git a/src/lib/Bcfg2/Server/Plugin/__init__.py b/src/lib/Bcfg2/Server/Plugin/__init__.py index e28e458b3..69fc90b2f 100644 --- a/src/lib/Bcfg2/Server/Plugin/__init__.py +++ b/src/lib/Bcfg2/Server/Plugin/__init__.py @@ -11,7 +11,6 @@ documentation it's not necessary to use the submodules. E.g., you can from Bcfg2.Server.Plugin.base import Plugin """ - import Bcfg2.Options # pylint: disable=W0401 diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py index 5cfc8998c..6b521dfd6 100644 --- a/src/lib/Bcfg2/Server/Plugin/helpers.py +++ b/src/lib/Bcfg2/Server/Plugin/helpers.py @@ -1064,7 +1064,22 @@ class PrioDir(Plugin, Generator, XMLDirectoryBacked): data = candidate break - entry.text = data.text + self._apply(entry, data) + + def _apply(self, entry, data): + """ Apply all available values from data onto entry. This + sets the available attributes (for all attribues unset in + the entry), adds all children and copies the text from data + to entry. + + :param entry: The entry to apply the changes + :type entry: lxml.etree._Element + :param data: The entry to get the data from + :type data: lxml.etree._Element + """ + + if data.text is not None and data.text.strip() != '': + entry.text = data.text for item in data.getchildren(): entry.append(copy.copy(item)) diff --git a/src/lib/Bcfg2/Server/Plugins/Bundler.py b/src/lib/Bcfg2/Server/Plugins/Bundler.py index e38eeea89..f5bcbe797 100644 --- a/src/lib/Bcfg2/Server/Plugins/Bundler.py +++ b/src/lib/Bcfg2/Server/Plugins/Bundler.py @@ -92,6 +92,10 @@ class Bundler(Plugin, self.logger.error("Bundler: Failed to render templated bundle " "%s: %s" % (bundlename, err)) continue + except: + self.logger.error("Bundler: Unexpected bundler error for %s" % + bundlename, exc_info=1) + continue if data.get("independent", "false").lower() == "true": data.tag = "Independent" @@ -124,7 +128,7 @@ class Bundler(Plugin, # dependent bundle -- add it to the list of # bundles for this client if child.get("name") not in bundles_added: - bundles.append(child.get("name")) + bundles.add(child.get("name")) bundles_added.add(child.get("name")) if child.get('inherit_modification', 'false') == 'true': if metadata.version_info >= \ diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgJinja2Generator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgJinja2Generator.py index cff9ff61e..71aec7658 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgJinja2Generator.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgJinja2Generator.py @@ -12,14 +12,14 @@ from Bcfg2.Server.Plugins.Cfg import CfgGenerator try: from jinja2 import Environment, FileSystemLoader HAS_JINJA2 = True -except ImportError: - HAS_JINJA2 = False + class RelEnvironment(Environment): + """Override join_path() to enable relative template paths.""" + def join_path(self, template, parent): + return os.path.join(os.path.dirname(parent), template) -class RelEnvironment(Environment): - """Override join_path() to enable relative template paths.""" - def join_path(self, template, parent): - return os.path.join(os.path.dirname(parent), template) +except ImportError: + HAS_JINJA2 = False class DefaultJinja2DataProvider(DefaultTemplateDataProvider): @@ -42,15 +42,16 @@ class CfgJinja2Generator(CfgGenerator): #: Handle .jinja2 files __extensions__ = ['jinja2'] - #: ``__loader_cls__`` is the class that will be instantiated to - #: load the template files. It must implement one public function, - #: ``load()``, as :class:`genshi.template.TemplateLoader`. - __loader_cls__ = FileSystemLoader + if HAS_JINJA2: + #: ``__loader_cls__`` is the class that will be instantiated to + #: load the template files. It must implement one public function, + #: ``load()``, as :class:`genshi.template.TemplateLoader`. + __loader_cls__ = FileSystemLoader - #: ``__environment_cls__`` is the class that will be instantiated to - #: store the jinja2 environment. It must implement one public function, - #: ``get_template()``, as :class:`jinja2.Environment`. - __environment_cls__ = RelEnvironment + #: ``__environment_cls__`` is the class that will be instantiated to + #: store the jinja2 environment. It must implement one public + #: function, ``get_template()``, as :class:`jinja2.Environment`. + __environment_cls__ = RelEnvironment #: Ignore ``.jinja2_include`` files so they can be used with the #: Jinja2 ``{% include ... %}`` directive without raising warnings. @@ -68,7 +69,15 @@ class CfgJinja2Generator(CfgGenerator): encoding = Bcfg2.Options.setup.encoding self.loader = self.__loader_cls__('/', encoding=encoding) - self.environment = self.__environment_cls__(loader=self.loader) + try: + # keep_trailing_newline is new in Jinja2 2.7, and will + # fail with earlier versions + self.environment = \ + self.__environment_cls__(loader=self.loader, + keep_trailing_newline=True) + except TypeError: + self.environment = \ + self.__environment_cls__(loader=self.loader) __init__.__doc__ = CfgGenerator.__init__.__doc__ def get_data(self, entry, metadata): diff --git a/src/lib/Bcfg2/Server/Plugins/Defaults.py b/src/lib/Bcfg2/Server/Plugins/Defaults.py index 79e2ca0e2..2242e3825 100644 --- a/src/lib/Bcfg2/Server/Plugins/Defaults.py +++ b/src/lib/Bcfg2/Server/Plugins/Defaults.py @@ -1,5 +1,6 @@ """This generator provides rule-based entry mappings.""" +import Bcfg2.Options import Bcfg2.Server.Plugin import Bcfg2.Server.Plugins.Rules @@ -9,7 +10,10 @@ class Defaults(Bcfg2.Server.Plugins.Rules.Rules, """Set default attributes on bound entries""" __author__ = 'bcfg-dev@mcs.anl.gov' - options = Bcfg2.Server.Plugin.PrioDir.options + options = Bcfg2.Server.Plugin.PrioDir.options + [ + Bcfg2.Options.BooleanOption( + cf=("defaults", "replace_name"), dest="defaults_replace_name", + help="Replace %{name} in attributes with name of target entry")] # Rules is a Generator that happens to implement all of the # functionality we want, so we overload it, but Defaults should @@ -41,3 +45,9 @@ class Defaults(Bcfg2.Server.Plugins.Rules.Rules, def _regex_enabled(self): """ Defaults depends on regex matching, so force it enabled """ return True + + @property + def _replace_name_enabled(self): + """ Return True if the replace_name feature is enabled, + False otherwise """ + return Bcfg2.Options.setup.defaults_replace_name diff --git a/src/lib/Bcfg2/Server/Plugins/Ldap.py b/src/lib/Bcfg2/Server/Plugins/Ldap.py index 553ddbc47..66f317c20 100644 --- a/src/lib/Bcfg2/Server/Plugins/Ldap.py +++ b/src/lib/Bcfg2/Server/Plugins/Ldap.py @@ -1,120 +1,132 @@ +""" A plugin to fetch data from a LDAP directory """ + import imp -import logging +import os import sys import time import traceback +import Bcfg2.Options import Bcfg2.Server.Plugin - -logger = logging.getLogger('Bcfg2.Plugins.Ldap') +from Bcfg2.Logger import Debuggable +from Bcfg2.Utils import ClassName, safe_module_name try: import ldap + HAS_LDAP = True except ImportError: - logger.error("Unable to load ldap module. Is python-ldap installed?") - raise ImportError - -# time in seconds between retries after failed LDAP connection -RETRY_DELAY = 5 -# how many times to try reaching the LDAP server if a connection is broken -# at the very minimum, one retry is needed to handle a restarted LDAP daemon -RETRY_COUNT = 3 - -SCOPE_MAP = { - "base": ldap.SCOPE_BASE, - "one": ldap.SCOPE_ONELEVEL, - "sub": ldap.SCOPE_SUBTREE, -} - -LDAP_QUERIES = [] - - -def register_query(query): - LDAP_QUERIES.append(query) + HAS_LDAP = False class ConfigFile(Bcfg2.Server.Plugin.FileBacked): - """ - Config file for the Ldap plugin - - The config file cannot be 'parsed' in the traditional sense as we would - need some serious type checking ugliness to just get the LdapQuery - subclasses. The alternative would be to have the user create a list with - a predefined name that contains all queries. - The approach implemented here is having the user call a registering - decorator that updates a global variable in this module. - """ - def __init__(self, filename): - self.filename = filename - Bcfg2.Server.Plugin.FileBacked.__init__(self, self.filename) - self.fam.AddMonitor(self.filename, self) + """ Config file for the Ldap plugin """ + + def __init__(self, name, core): + Bcfg2.Server.Plugin.FileBacked.__init__(self, name) + self.core = core + self.queries = list() + self.fam.AddMonitor(name, self) def Index(self): - """ - Reregisters the queries in the config file + """ Get the queries from the config file """ + try: + module = imp.load_source(safe_module_name('Ldap', self.name), + self.name) + except: # pylint: disable=W0702 + err = sys.exc_info()[1] + self.logger.error("Ldap: Failed to import %s: %s" % + (self.name, err)) + return + + if not hasattr(module, "__queries__"): + self.logger.error("Ldap: %s has no __queries__ list" % self.name) + return + + self.queries = list() + for query in module.__queries__: + try: + self.queries.append(getattr(module, query)) + except AttributeError: + self.logger.warning( + "Ldap: %s exports %s, but has no such attribute" % + (self.name, query)) - The config will take care of actually registering the queries, - so we just load it once and don't keep it. - """ - global LDAP_QUERIES - LDAP_QUERIES = [] - imp.load_source("ldap_cfg", self.filename) + if self.core.metadata_cache_mode in ['cautious', 'aggressive']: + self.core.metadata_cache.expire() -class Ldap(Bcfg2.Server.Plugin.Plugin, Bcfg2.Server.Plugin.Connector): - """ - The Ldap plugin allows adding data from an LDAP server to your metadata. - """ - name = "Ldap" +class Ldap(Bcfg2.Server.Plugin.Plugin, + Bcfg2.Server.Plugin.ClientRunHooks, + Bcfg2.Server.Plugin.Connector): + """ The Ldap plugin allows adding data from an LDAP server + to your metadata. """ + experimental = True - debug_flag = False + + options = [ + Bcfg2.Options.Option( + cf=('ldap', 'retries'), type=int, default=3, + help='The number of times to retry reaching the ' + 'LDAP server if a connection is broken'), + Bcfg2.Options.Option( + cf=('ldap', 'retry_delay'), type=float, default=5.0, + help='The time in seconds betreen retries')] def __init__(self, core): Bcfg2.Server.Plugin.Plugin.__init__(self, core) Bcfg2.Server.Plugin.Connector.__init__(self) - self.config = ConfigFile(self.data + "/config.py") - def debug_log(self, message, flag=None): - if (flag is None) and self.debug_flag or flag: - self.logger.error(message) + if not HAS_LDAP: + msg = "Python ldap module is required for Ldap plugin" + self.logger.error(msg) + raise Bcfg2.Server.Plugin.PluginInitError(msg) + + self.config = ConfigFile(os.path.join(self.data, 'config.py')) def get_additional_data(self, metadata): query = None try: data = {} - self.debug_log("LdapPlugin debug: found queries " + - str(LDAP_QUERIES)) - for QueryClass in LDAP_QUERIES: - query = QueryClass() + self.debug_log("Found queries %s" % self.config.queries) + for query_class in self.config.queries: + query = query_class() if query.is_applicable(metadata): - self.debug_log("LdapPlugin debug: processing query '" + - query.name + "'") + self.debug_log("Processing query '%s'" % query.name) data[query.name] = query.get_result(metadata) else: - self.debug_log("LdapPlugin debug: query '" + query.name + - "' not applicable to host '" + - metadata.hostname + "'") + self.debug_log("query '%s' not applicable to host '%s'" % + (query.name, metadata.hostname)) return data - except Exception: + except: # pylint: disable=W0702 if hasattr(query, "name"): - logger.error("LdapPlugin error: " + - "Exception during processing of query named '" + - str(query.name) + - "', query results will be empty" + - " and may cause bind failures") - for line in traceback.format_exception(sys.exc_info()[0], - sys.exc_info()[1], - sys.exc_info()[2]): - logger.error("LdapPlugin error: " + - line.replace("\n", "")) + self.logger.error( + "Exception during processing of query named '%s', query " + "results will be empty and may cause bind failures" % + query.name) + for line in traceback.format_exc().split('\n'): + self.logger.error(line) return {} + def start_client_run(self, metadata): + if self.core.metadata_cache_mode == 'aggressive': + self.logger.warning("Ldap is incompatible with aggressive " + "client metadata caching, try 'cautious' " + "or 'initial'") + self.core.metadata_cache.expire(metadata.hostname) + + +class LdapConnection(Debuggable): + """ Connection to an LDAP server. """ + + __scopes__ = { + 'base': ldap.SCOPE_BASE, + 'one': ldap.SCOPE_ONELEVEL, + 'sub': ldap.SCOPE_SUBTREE, + } + + def __init__(self, host="localhost", port=389, binddn=None, + bindpw=None): + Debuggable.__init__(self) -class LdapConnection(object): - """ - Connection to an LDAP server. - """ - def __init__(self, host="localhost", port=389, - binddn=None, bindpw=None): self.host = host self.port = port self.binddn = binddn @@ -122,48 +134,62 @@ class LdapConnection(object): self.conn = None def __del__(self): + """ Disconnection if the instance is destroyed. """ + self.disconnect() + + def disconnect(self): + """ If a connection to an LDAP server is available, disconnect it. """ if self.conn: - self.conn.unbind() + self.conn.unbund() + self.conn = None - def init_conn(self): + def connect(self): + """ Open a connection to the configured LDAP server, and do a simple + bind ff both binddn and bindpw are set. """ + self.disconnect() self.conn = ldap.initialize(self.url) if self.binddn is not None and self.bindpw is not None: self.conn.simple_bind_s(self.binddn, self.bindpw) def run_query(self, query): - result = None - for attempt in range(RETRY_COUNT + 1): - if attempt >= 1: - logger.error("LdapPlugin error: " + - "LDAP server down (retry " + str(attempt) + "/" + - str(RETRY_COUNT) + ")") + """ Connect to the server and execute the query. If the server is + down, wait the configured amount and try to reconnect. + + :param query: The query to execute on the LDAP server. + :type query: Bcfg.Server.Plugins.Ldap.LdapQuery + """ + for attempt in range(Bcfg2.Options.setup.ldap_retries + 1): try: if not self.conn: - self.init_conn() - result = self.conn.search_s( - query.base, - SCOPE_MAP[query.scope], - query.filter.replace("\\", "\\\\"), - query.attrs, - ) - break + self.connect() + + return self.conn.search_s( + query.base, self.__scopes__[query.scope], + query.filter.replace('\\', '\\\\'), query.attrs) + except ldap.SERVER_DOWN: self.conn = None - time.sleep(RETRY_DELAY) - return result + self.logger.error( + "LdapConnection: Server %s down. Retry %d/%d in %.2fs." % + (self.url, attempt + 1, Bcfg2.Options.setup.ldap_retries, + Bcfg2.Options.setup.ldap_retry_delay)) + time.sleep(Bcfg2.Options.setup.ldap_retry_delay) + + return None @property def url(self): - return "ldap://" + self.host + ":" + str(self.port) + """ The URL of the LDAP server. """ + return "ldap://%s:%d" % (self.host, self.port) class LdapQuery(object): - """ - Query referencing an LdapConnection and providing several - methods for query manipulation. - """ + """ Query referencing an LdapConnection and providing several + methods for query manipulation. """ + + #: Name of the Query, used to register it in additional data. + name = ClassName() - name = "unknown" base = "" scope = "sub" filter = "(objectClass=*)" @@ -172,80 +198,48 @@ class LdapQuery(object): result = None def __unicode__(self): - return "LdapQuery:" + self.name + return "LdapQuery: %s" % self.name - def is_applicable(self, metadata): - """ - Overrideable method to determine if the query is to be executed for - the given metadata object. - Defaults to true. - """ - return True + def is_applicable(self, metadata): # pylint: disable=W0613 + """ Check is the query should be executed for a given metadata + object. - def prepare_query(self, metadata): + :param metadata: The client metadata + :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata """ - Overrideable method to alter the query based on metadata. - Defaults to doing nothing. - - In most cases, you will do something like + return True - self.filter = "(cn=" + metadata.hostname + ")" + 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. - here. + :param metadata: The client metadata + :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata """ pass - def process_result(self, metadata): - """ - Overrideable method to post-process the query result. - Defaults to returning the unaltered result. - """ - return self.result - - def get_result(self, metadata): - """ - Method to handle preparing, executing and processing the query. - """ - if isinstance(self.connection, LdapConnection): - self.prepare_query(metadata) - self.result = self.connection.run_query(self) - self.result = self.process_result(metadata) - return self.result - else: - logger.error("LdapPlugin error: " + - "No valid connection defined for query " + str(self)) - return None - - -class LdapSubQuery(LdapQuery): - """ - SubQueries are meant for internal use only and are not added - to the metadata object. They are useful for situations where - you need to run more than one query to obtain some data. - """ - def prepare_query(self, metadata, **kwargs): - """ - Overrideable method to alter the query based on metadata. - Defaults to doing nothing. - """ - pass + def process_result(self, metadata, **kwargs): # pylint: disable=W0613 + """ Post-process the query result. - def process_result(self, metadata, **kwargs): - """ - Overrideable method to post-process the query result. - Defaults to returning the unaltered 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` """ - Method to handle preparing, executing and processing the query. - """ - if isinstance(self.connection, LdapConnection): + + if self.connection is not None: self.prepare_query(metadata, **kwargs) self.result = self.connection.run_query(self) - return self.process_result(metadata, **kwargs) + self.result = self.process_result(metadata, **kwargs) else: - logger.error("LdapPlugin error: " + - "No valid connection defined for query " + str(self)) - return None + raise Bcfg2.Server.Plugin.PluginExecutionError( + 'No connection defined for %s' % self.name) + + return self.result diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py index b850c1870..657e4df31 100644 --- a/src/lib/Bcfg2/Server/Plugins/Metadata.py +++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py @@ -1394,8 +1394,6 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, # look at cert.cN client = certinfo['commonName'] self.debug_log("Got cN %s; using as client name" % client) - auth_type = self.auth.get(client, - Bcfg2.Options.setup.authentication) elif user == 'root': id_method = 'address' try: @@ -1417,6 +1415,13 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, # we have the client name self.debug_log("Authenticating client %s" % client) + # validate id_method + auth_type = self.auth.get(client, Bcfg2.Options.setup.authentication) + if auth_type == 'cert' and id_method != 'cert': + self.logger.error("Client %s does not provide a cert, but only " + "cert auth is allowed" % client) + return False + # next we validate the address if (id_method != 'uuid' and not self.validate_client_address(client, address)): diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py index c9f6ea14a..86f7698f7 100644 --- a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py +++ b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py @@ -135,22 +135,22 @@ class Source(Debuggable): # pylint: disable=R0902 #: A list of the text of all 'Component' attributes of this #: source from XML - self.components = [item.text for item in xsource.findall('Component')] + self.components = [] #: A list of the arches supported by this source - self.arches = [item.text for item in xsource.findall('Arch')] + self.arches = [] #: A list of the the names of packages that are blacklisted #: from this source - self.blacklist = [item.text for item in xsource.findall('Blacklist')] + self.blacklist = [] #: A list of the the names of packages that are whitelisted in #: this source - self.whitelist = [item.text for item in xsource.findall('Whitelist')] + self.whitelist = [] #: Whether or not to include deb-src lines in the generated APT #: configuration - self.debsrc = xsource.get('debsrc', 'false') == 'true' + self.debsrc = False #: A dict of repository options that will be included in the #: configuration generated on the server side (if such is @@ -162,51 +162,38 @@ class Source(Debuggable): # pylint: disable=R0902 #: configuration generated for the client (if that is #: supported by the backend) self.client_options = dict() - opts = xsource.findall("Options") - for el in opts: - repoopts = dict([(k, v) - for k, v in el.attrib.items() - if k != "clientonly" and k != "serveronly"]) - if el.get("clientonly", "false").lower() == "false": - self.server_options.update(repoopts) - if el.get("serveronly", "false").lower() == "false": - self.client_options.update(repoopts) #: A list of URLs to GPG keys that apply to this source - self.gpgkeys = [el.text for el in xsource.findall("GPGKey")] + self.gpgkeys = [] #: Whether or not to include essential packages from this source - self.essential = xsource.get('essential', 'true').lower() == 'true' + self.essential = True #: Whether or not to include recommended packages from this source - self.recommended = xsource.get('recommended', - 'false').lower() == 'true' + self.recommended = False #: The "rawurl" attribute from :attr:`xsource`, if applicable. #: A trailing slash is automatically appended to this if there #: wasn't one already present. - self.rawurl = xsource.get('rawurl', '') - if self.rawurl and not self.rawurl.endswith("/"): - self.rawurl += "/" + self.rawurl = None #: The "url" attribute from :attr:`xsource`, if applicable. A #: trailing slash is automatically appended to this if there #: wasn't one already present. - self.url = xsource.get('url', '') - if self.url and not self.url.endswith("/"): - self.url += "/" + self.url = None #: The "version" attribute from :attr:`xsource` - self.version = xsource.get('version', '') + self.version = None #: The "name" attribute from :attr:`xsource` - self.name = xsource.get('name', None) + self.name = None #: A list of predicates that are used to determine if this #: source applies to a given #: :class:`Bcfg2.Server.Plugins.Metadata.ClientMetadata` #: object. self.conditions = [] + #: Formerly, :ref:`server-plugins-generators-packages` only #: supported applying package sources to groups; that is, they #: could not be assigned by more complicated logic like @@ -214,22 +201,6 @@ class Source(Debuggable): # pylint: disable=R0902 #: attribute attempts to provide for some limited backwards #: compat with older code that relies on this. self.groups = [] - for el in xsource.iterancestors(): - if el.tag == "Group": - if el.get("negate", "false").lower() == "true": - self.conditions.append(lambda m, el=el: - el.get("name") not in m.groups) - else: - self.groups.append(el.get("name")) - self.conditions.append(lambda m, el=el: - el.get("name") in m.groups) - elif el.tag == "Client": - if el.get("negate", "false").lower() == "true": - self.conditions.append(lambda m, el=el: - el.get("name") != m.hostname) - else: - self.conditions.append(lambda m, el=el: - el.get("name") == m.hostname) #: A set of all package names in this source. This will not #: necessarily be populated, particularly by backends that @@ -253,6 +224,8 @@ class Source(Debuggable): # pylint: disable=R0902 #: symbols>``. This will not necessarily be populated. self.recommends = dict() + self._init_attributes(xsource) + #: The file (or directory) used for this source's cache data self.cachefile = os.path.join(self.basepath, "cache-%s" % self.cachekey) @@ -292,6 +265,69 @@ class Source(Debuggable): # pylint: disable=R0902 setting['name'] = self.get_repo_name(setting) self.url_map.extend(usettings) + def _init_attributes(self, xsource): + """ + This functions evaluates the Source tag and parses all + attributes. Override this function in a sub class to + parse specific attributes. Do not use ``__init__`` because + ``Source.__init__`` may call other functions that already + need this specific fields. This functions is called before + any other function. + + :param xsource: The XML tag that describes this source + :type source: lxml.etree._Element + """ + + self.components = [item.text for item in xsource.findall('Component')] + self.arches = [item.text for item in xsource.findall('Arch')] + self.blacklist = [item.text for item in xsource.findall('Blacklist')] + self.whitelist = [item.text for item in xsource.findall('Whitelist')] + self.debsrc = xsource.get('debsrc', 'false') == 'true' + + opts = xsource.findall("Options") + for el in opts: + repoopts = dict([(k, v) + for k, v in el.attrib.items() + if k != "clientonly" and k != "serveronly"]) + if el.get("clientonly", "false").lower() == "false": + self.server_options.update(repoopts) + if el.get("serveronly", "false").lower() == "false": + self.client_options.update(repoopts) + + self.gpgkeys = [el.text for el in xsource.findall("GPGKey")] + + self.essential = xsource.get('essential', 'true').lower() == 'true' + self.recommended = xsource.get('recommended', + 'false').lower() == 'true' + + self.rawurl = xsource.get('rawurl', '') + if self.rawurl and not self.rawurl.endswith("/"): + self.rawurl += "/" + + self.url = xsource.get('url', '') + if self.url and not self.url.endswith("/"): + self.url += "/" + + self.version = xsource.get('version', '') + self.name = xsource.get('name', None) + + for el in xsource.iterancestors(): + if el.tag == "Group": + if el.get("negate", "false").lower() == "true": + self.conditions.append(lambda m, el=el: + el.get("name") not in m.groups) + else: + self.groups.append(el.get("name")) + self.conditions.append(lambda m, el=el: + el.get("name") in m.groups) + elif el.tag == "Client": + if el.get("negate", "false").lower() == "true": + self.conditions.append(lambda m, el=el: + el.get("name") != m.hostname) + else: + self.conditions.append(lambda m, el=el: + el.get("name") == m.hostname) + @property def cachekey(self): """ A unique key for this source that will be used to generate diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py index dbe3f9ce5..14d6db8a0 100644 --- a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py +++ b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py @@ -1004,8 +1004,20 @@ class YumSource(Source): ptype = 'yum' def __init__(self, basepath, xsource): - Source.__init__(self, basepath, xsource) + self.filemap = dict() + self.file_to_arch = dict() + self.needed_paths = set() + self.packages = dict() + self.yumgroups = dict() self.pulp_id = None + self.repo = None + + Source.__init__(self, basepath, xsource) + __init__.__doc__ = Source.__init__.__doc__ + + def _init_attributes(self, xsource): + Source._init_attributes(self, xsource) + if HAS_PULP and xsource.get("pulp_id"): self.pulp_id = xsource.get("pulp_id") @@ -1034,15 +1046,11 @@ class YumSource(Source): self.repo['relative_path']) self.arches = [self.repo['arch']] - self.packages = dict() self.deps = dict([('global', dict())]) self.provides = dict([('global', dict())]) self.filemap = dict([(x, dict()) for x in ['global'] + self.arches]) - self.needed_paths = set() - self.file_to_arch = dict() - self.yumgroups = dict() - __init__.__doc__ = Source.__init__.__doc__ + _init_attributes.__doc__ = Source._init_attributes.__doc__ @property def use_yum(self): @@ -1130,6 +1138,94 @@ class YumSource(Source): self.file_to_arch[self.escape_url(fullurl)] = arch return urls + # pylint: disable=R0911,R0912 + # disabling the pylint errors above because we are interesting in + # replicating the flow of the RPM code. + def _compare_rpm_versions(self, str1, str2): + """ Compare RPM versions. + + This is an attempt to reimplement RPM's rpmvercmp method in python. + + :param str1: package 1 version string + :param str2: package 2 version string + :return: 1 - str1 is newer than str2 + 0 - str1 and str2 are the same version + -1 - str2 is newer than str1""" + if str1 == str2: + return 0 + + front_strip_re = re.compile('^[^A-Za-z0-9~]+') + risdigit = re.compile('(^[0-9]+)') + risalpha = re.compile('(^[A-Za-z])') + lzeroes = re.compile('^0+') + + while len(str1) > 0 or len(str2) > 0: + str1 = front_strip_re.sub('', str1) + str2 = front_strip_re.sub('', str2) + + if len(str1) == 0 or len(str2) == 0: + break + + # handle the tilde separator + if str1[0] == '~' and str2[0] == '~': + str1 = str1[1:] + str2 = str2[1:] + elif str1[0] == '~': + return -1 + elif str2[0] == '~': + return 1 + + # grab continuous segments from each string + isnum = False + if risdigit.match(str1): + segment1 = risdigit.split(str1)[1] + str1 = risdigit.split(str1)[2] + if risdigit.match(str2): + segment2 = risdigit.split(str2)[1] + str2 = risdigit.split(str2)[2] + else: + segment2 = '' + isnum = True + else: + segment1 = risalpha.split(str1)[1] + str1 = risalpha.split(str1)[2] + if risalpha.match(str2): + segment2 = risalpha.split(str2)[1] + str2 = risalpha.split(str2)[2] + else: + segment2 = '' + + # numeric segments are always newer than alpha segments + if len(segment2) == 0: + if isnum: + return 1 + return -1 + + if isnum: + # discard leading zeroes + segment1 = lzeroes.sub('', segment1) + segment2 = lzeroes.sub('', segment2) + # higher number has more digits + if len(segment1) > len(segment2): + return 1 + elif len(segment2) > len(segment1): + return -1 + # do a simple string comparison + if segment1 > segment2: + return 1 + elif segment2 > segment1: + return -1 + + # if one of the strings is empty, the version of the longer + # string is higher + if len(str1) > len(str2): + return 1 + elif len(str2) > len(str1): + return -1 + else: + return 0 + # pylint: enable=R0911,R0912 + @track_statistics() def read_files(self): """ When using the builtin yum parser, read and parse locally @@ -1198,13 +1294,33 @@ class YumSource(Source): if arch not in self.packages: self.packages[arch] = set() if arch not in self.deps: - self.deps[arch] = dict() + self.deps[arch] = {} if arch not in self.provides: - self.provides[arch] = dict() + self.provides[arch] = {} + versionmap = {} for pkg in data.getchildren(): if not pkg.tag.endswith('package'): continue pkgname = pkg.find(XP + 'name').text + vtag = pkg.find(XP + 'version') + epoch = vtag.get('epoch') + version = vtag.get('ver') + release = vtag.get('rel') + if pkgname in self.packages[arch]: + # skip if version older than a previous version + if (self._compare_rpm_versions( + epoch, versionmap[pkgname]['epoch']) < 0): + continue + elif (self._compare_rpm_versions( + version, versionmap[pkgname]['version']) < 0): + continue + elif (self._compare_rpm_versions( + release, versionmap[pkgname]['release']) < 0): + continue + versionmap[pkgname] = {} + versionmap[pkgname]['epoch'] = epoch + versionmap[pkgname]['version'] = version + versionmap[pkgname]['release'] = release self.packages[arch].add(pkgname) pdata = pkg.find(XP + 'format') @@ -1256,10 +1372,15 @@ class YumSource(Source): arch = [a for a in self.arches if a in metadata.groups] if not arch: return False - return ((package in self.packages['global'] or - package in self.packages[arch[0]]) and - package not in self.blacklist and - (len(self.whitelist) == 0 or package in self.whitelist)) + try: + return ((package in self.packages['global'] or + package in self.packages[arch[0]]) and + package not in self.blacklist and + (len(self.whitelist) == 0 or package in self.whitelist)) + except KeyError: + self.logger.debug("Packages: Unable to find %s for arch %s" % + (package, arch[0])) + return False is_package.__doc__ = Source.is_package.__doc__ def get_vpkgs(self, metadata): diff --git a/src/lib/Bcfg2/Server/Plugins/Rules.py b/src/lib/Bcfg2/Server/Plugins/Rules.py index a3f682ed6..cf659251c 100644 --- a/src/lib/Bcfg2/Server/Plugins/Rules.py +++ b/src/lib/Bcfg2/Server/Plugins/Rules.py @@ -1,10 +1,17 @@ """This generator provides rule-based entry mappings.""" +import copy import re +import string import Bcfg2.Options import Bcfg2.Server.Plugin +class NameTemplate(string.Template): + """Simple subclass of string.Template with a custom delimiter.""" + delimiter = '%' + + class Rules(Bcfg2.Server.Plugin.PrioDir): """This is a generator that handles service assignments.""" __author__ = 'bcfg-dev@mcs.anl.gov' @@ -12,7 +19,10 @@ class Rules(Bcfg2.Server.Plugin.PrioDir): options = Bcfg2.Server.Plugin.PrioDir.options + [ Bcfg2.Options.BooleanOption( cf=("rules", "regex"), dest="rules_regex", - help="Allow regular expressions in Rules")] + help="Allow regular expressions in Rules"), + Bcfg2.Options.BooleanOption( + cf=("rules", "replace_name"), dest="rules_replace_name", + help="Replace %{name} in attributes with name of target entry")] def __init__(self, core): Bcfg2.Server.Plugin.PrioDir.__init__(self, core) @@ -46,7 +56,22 @@ class Rules(Bcfg2.Server.Plugin.PrioDir): return True return False + def _apply(self, entry, data): + if self._replace_name_enabled: + data = copy.deepcopy(data) + for key, val in list(data.attrib.items()): + data.attrib[key] = NameTemplate(val).safe_substitute( + name=entry.get('name')) + + Bcfg2.Server.Plugin.PrioDir._apply(self, entry, data) + @property def _regex_enabled(self): """ Return True if rules regexes are enabled, False otherwise """ return Bcfg2.Options.setup.rules_regex + + @property + def _replace_name_enabled(self): + """ Return True if the replace_name feature is enabled, + False otherwise """ + return Bcfg2.Options.setup.rules_replace_name diff --git a/src/lib/Bcfg2/Server/Plugins/SSHbase.py b/src/lib/Bcfg2/Server/Plugins/SSHbase.py index e4fb9b565..7736bd050 100644 --- a/src/lib/Bcfg2/Server/Plugins/SSHbase.py +++ b/src/lib/Bcfg2/Server/Plugins/SSHbase.py @@ -103,6 +103,7 @@ class KnownHostsEntrySet(Bcfg2.Server.Plugin.EntrySet): class SSHbase(Bcfg2.Server.Plugin.Plugin, + Bcfg2.Server.Plugin.Connector, Bcfg2.Server.Plugin.Generator, Bcfg2.Server.Plugin.PullTarget): """ @@ -141,6 +142,7 @@ class SSHbase(Bcfg2.Server.Plugin.Plugin, def __init__(self, core): Bcfg2.Server.Plugin.Plugin.__init__(self, core) + Bcfg2.Server.Plugin.Connector.__init__(self) Bcfg2.Server.Plugin.Generator.__init__(self) Bcfg2.Server.Plugin.PullTarget.__init__(self) self.ipcache = {} @@ -210,7 +212,7 @@ class SSHbase(Bcfg2.Server.Plugin.Plugin, try: names[cmeta.hostname].update( self.get_namecache_entry(ip)) - except socket.gaierror: + except socket.herror: continue names[cmeta.hostname] = sorted(names[cmeta.hostname]) @@ -332,7 +334,7 @@ class SSHbase(Bcfg2.Server.Plugin.Plugin, self.ipcache[client] = False msg = "Failed to find IP address for %s: %s" % (client, result.error) - self.logger(msg) + self.logger.error(msg) raise PluginExecutionError(msg) def get_namecache_entry(self, cip): @@ -342,7 +344,7 @@ class SSHbase(Bcfg2.Server.Plugin.Plugin, if self.namecache[cip]: return self.namecache[cip] else: - raise socket.gaierror + raise socket.herror else: # add an entry that has not been cached try: @@ -353,7 +355,7 @@ class SSHbase(Bcfg2.Server.Plugin.Plugin, self.namecache[cip] = [] self.namecache[cip].extend(rvlookup[1]) return self.namecache[cip] - except socket.gaierror: + except socket.herror: self.namecache[cip] = False self.logger.error("Failed to find any names associated with " "IP address %s" % cip) @@ -489,3 +491,15 @@ class SSHbase(Bcfg2.Server.Plugin.Plugin, self.logger.error("Failed to pull %s. This file does not " "currently exist on the client" % entry.get('name')) + + def get_additional_data(self, metadata): + data = dict() + for key in self.keypatterns: + if key.endswith(".pub"): + try: + keyfile = "/etc/ssh/" + key + entry = self.entries[keyfile].best_matching(metadata) + data[key] = entry.data + except Bcfg2.Server.Plugin.PluginExecutionError: + pass + return data diff --git a/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py b/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py index cec2de297..b0b8d0061 100644 --- a/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py +++ b/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py @@ -7,18 +7,11 @@ import lxml.etree from Bcfg2.Server.Plugin import Plugin, Connector, DirectoryBacked, \ TemplateDataProvider, DefaultTemplateDataProvider from Bcfg2.Logger import Debuggable +from Bcfg2.Utils import safe_module_name MODULE_RE = re.compile(r'(?P<filename>(?P<module>[^\/]+)\.py)$') -def safe_module_name(module): - """ Munge the name of a TemplateHelper module to avoid collisions - with other Python modules. E.g., if someone has a helper named - 'ldap.py', it should not be added to ``sys.modules`` as ``ldap``, - but rather as something more obscure. """ - return '__TemplateHelper_%s' % module - - class HelperModule(Debuggable): """ Representation of a TemplateHelper module """ @@ -52,8 +45,9 @@ class HelperModule(Debuggable): return try: - module = imp.load_source(safe_module_name(self._module_name), - self.name) + module = imp.load_source( + safe_module_name('TemplateHelper', self._module_name), + self.name) except: # pylint: disable=W0702 # this needs to be a blanket except because the # imp.load_source() call can raise literally any error, diff --git a/src/lib/Bcfg2/Utils.py b/src/lib/Bcfg2/Utils.py index 10057b63e..2fdc0c3e0 100644 --- a/src/lib/Bcfg2/Utils.py +++ b/src/lib/Bcfg2/Utils.py @@ -321,6 +321,15 @@ def safe_input(msg): return input(msg) +def safe_module_name(prefix, module): + """ Munge the name of a module with prefix to avoid collisions + with other Python modules. E.g., if you want to import user + defined helper modules and someone has a helper named 'ldap.py', + it should not be added to ``sys.modules`` as ``ldap``, but rather + as something more obscure. """ + return '__%s_%s' % (prefix, module) + + class classproperty(object): # pylint: disable=C0103 """ Decorator that can be used to create read-only class properties. """ @@ -330,3 +339,19 @@ class classproperty(object): # pylint: disable=C0103 def __get__(self, instance, owner): return self.getter(owner) + + +def is_string(strng, encoding): + """ Returns true if the string contains no ASCII control + characters and can be decoded from the specified encoding. """ + for char in strng: + if ord(char) < 9 or ord(char) > 13 and ord(char) < 32: + return False + if not hasattr(strng, "decode"): + # py3k + return True + try: + strng.decode(encoding) + return True + except: # pylint: disable=W0702 + return False diff --git a/src/sbin/bcfg2-info b/src/sbin/bcfg2-info index adfa96852..3d9b0d87b 100755 --- a/src/sbin/bcfg2-info +++ b/src/sbin/bcfg2-info @@ -4,5 +4,6 @@ import sys from Bcfg2.Server.Info import CLI + if __name__ == '__main__': sys.exit(CLI().run()) diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py index 69dd562be..47d3b84ed 100644 --- a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py +++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py @@ -37,22 +37,6 @@ class TestPOSIXFile(TestPOSIXTool): entry.text = "text" self.assertTrue(ptool.fully_specified(entry)) - def test_is_string(self): - ptool = self.get_obj() - - for char in list(range(8)) + list(range(14, 32)): - self.assertFalse(ptool._is_string("foo" + chr(char) + "bar", - 'UTF-8')) - for char in list(range(9, 14)) + list(range(33, 128)): - self.assertTrue(ptool._is_string("foo" + chr(char) + "bar", - 'UTF-8')) - ustr = 'é' - self.assertTrue(ptool._is_string(ustr, 'UTF-8')) - if not inPy3k: - self.assertFalse(ptool._is_string("foo" + chr(128) + "bar", - 'ascii')) - self.assertFalse(ptool._is_string(ustr, 'ascii')) - def test_get_data(self): orig_entry = lxml.etree.Element("Path", name="/test", type="file") Bcfg2.Options.setup.encoding = "ascii" @@ -216,7 +200,8 @@ class TestPOSIXFile(TestPOSIXTool): mock_unlink.assert_called_with(newfile) @patch("%s.open" % builtins) - def test__get_diffs(self, mock_open): + @patch("Bcfg2.Utils") + def test__get_diffs(self, mock_utils, mock_open): orig_entry = lxml.etree.Element("Path", name="/test", type="file", mode='0644', owner='root', group='root') @@ -226,16 +211,15 @@ class TestPOSIXFile(TestPOSIXTool): ptool = self.get_obj() ptool._get_data = Mock() ptool._diff = Mock() - ptool._is_string = Mock() def reset(): - ptool._is_string.reset_mock() + mock_utils.is_string.reset_mock() ptool._get_data.reset_mock() ptool._diff.reset_mock() mock_open.reset_mock() return copy.deepcopy(orig_entry) - ptool._is_string.return_value = True + mock_utils.is_string.return_value = True ptool._get_data.return_value = (orig_entry.text, False) mock_open.return_value.read.return_value = ondisk ptool._diff.return_value = ["-test2", "+test"] @@ -250,7 +234,7 @@ class TestPOSIXFile(TestPOSIXTool): # binary data on disk entry = reset() - ptool._is_string.return_value = False + mock_utils.is_string.return_value = False ptool._get_diffs(entry, content=ondisk) self.assertFalse(mock_open.called) self.assertFalse(ptool._diff.called) @@ -258,7 +242,7 @@ class TestPOSIXFile(TestPOSIXTool): # sensitive, non-interactive -- do nothing entry = reset() - ptool._is_string.return_value = True + mock_utils.is_string.return_value = True ptool._get_diffs(entry, sensitive=True, interactive=False) self.assertFalse(mock_open.called) self.assertFalse(ptool._diff.called) diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestComponents.py b/testsuite/Testsrc/Testlib/TestOptions/TestComponents.py index 61b87de2a..b1ed4cb2b 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/TestComponents.py +++ b/testsuite/Testsrc/Testlib/TestOptions/TestComponents.py @@ -182,7 +182,8 @@ class TestImportComponentOptions(OptionTestCase): """test cases for component loading.""" def setUp(self): - self.options = [Option("--cls", action=ImportComponentAction), + self.options = [Option("--cls", cf=("config", "cls"), + action=ImportComponentAction), Option("--module", action=ImportModuleAction)] self.result = argparse.Namespace() @@ -227,3 +228,10 @@ class TestImportComponentOptions(OptionTestCase): self.assertRaises(SystemExit, self.parser.parse, ["-C", config_file, "--cls", "Bcfg2.No.Such.Thing"]) + + @make_config({"config": {"test": "foo", "cls": "Two"}}) + def test_default_from_config_for_component_options(self, config_file): + """use default value from config file for options added by dynamic loaded component.""" + self.parser.parse(["-C", config_file]) + self.assertEqual(self.result.cls, Two.Two) + self.assertEqual(self.result.test, "foo") diff --git a/testsuite/Testsrc/Testlib/TestOptions/TestTypes.py b/testsuite/Testsrc/Testlib/TestOptions/TestTypes.py index 404d67fdc..0b67db38a 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/TestTypes.py +++ b/testsuite/Testsrc/Testlib/TestOptions/TestTypes.py @@ -46,21 +46,23 @@ class TestOptionTypes(Bcfg2TestCase): self.assertItemsEqual(self._test_options(["--test", "one:two three"]), ["one", "two three"]) - def test_comma_dict(self): - """parse comma-dict values.""" - self.options = [Option("--test", type=Types.comma_dict)] + def test_literal_dict(self): + """parse literal-dict values.""" + self.options = [Option("--test", type=Types.literal_dict)] expected = { "one": True, "two": 2, "three": "three", - "four": False} + "four": False, + "five": { + "a": 1, + "b": 2 + }} self.assertDictEqual( self._test_options(["--test", - "one=yes, two=2 , three=three,four=no"]), - expected) - - self.assertDictEqual( - self._test_options(["--test", "one,two=2,three=three,four=off"]), + '''{ "one": True, "two": 2, + "three": "three", "four": False, + "five": { "a": 1, "b": 2 }}''']), expected) def test_anchored_regex_list(self): diff --git a/testsuite/Testsrc/Testlib/TestOptions/Two.py b/testsuite/Testsrc/Testlib/TestOptions/Two.py index 189e0817f..0120e8b77 100644 --- a/testsuite/Testsrc/Testlib/TestOptions/Two.py +++ b/testsuite/Testsrc/Testlib/TestOptions/Two.py @@ -1,6 +1,7 @@ """Test module for component loading.""" +from Bcfg2.Options import Option class Two(object): """Test class for component loading.""" - pass + options = [Option('--test', cf=("config", "test"), dest="test", default="bar")] diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py index 1bf208c3e..5a8c44cd5 100644 --- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py +++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py @@ -98,7 +98,7 @@ class TestBundler(TestPlugin, TestStructure, TestXMLDirectoryBacked): lxml.etree.SubElement(expected['indep'], "Service", name="baz") metadata = Mock() - metadata.bundles = ["error", "xinclude", "has_dep", "indep"] + metadata.bundles = set(["error", "xinclude", "has_dep", "indep"]) metadata.version_info = Bcfg2VersionInfo('1.4.0') rv = b.BuildStructures(metadata) @@ -131,7 +131,7 @@ class TestBundler(TestPlugin, TestStructure, TestXMLDirectoryBacked): lxml.etree.SubElement(expected['has_dep'], "Package", name="foo") metadata = Mock() - metadata.bundles = ["has_dep"] + metadata.bundles = set(["has_dep"]) metadata.version_info = Bcfg2VersionInfo('1.3.0') rv = b.BuildStructures(metadata) diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestDefaults.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestDefaults.py index 9b4a6af88..3c660099e 100644 --- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestDefaults.py +++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestDefaults.py @@ -22,6 +22,10 @@ from Testinterfaces import TestGoalValidator class TestDefaults(TestRules, TestGoalValidator): test_obj = Defaults + def setUp(self): + TestRules.setUp(self) + set_setup_default("defaults_replace_name", True) + def get_obj(self, *args, **kwargs): return TestRules.get_obj(self, *args, **kwargs) @@ -91,3 +95,8 @@ class TestDefaults(TestRules, TestGoalValidator): def test_regex(self): self._do_test('regex') + + def test_replace_name(self): + Bcfg2.Options.setup.defaults_replace_name = True + self._do_test('replace_name') + Bcfg2.Options.setup.defaults_replace_name = False diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestRules.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestRules.py index 45f3671e8..88b334edf 100644 --- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestRules.py +++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestRules.py @@ -32,6 +32,7 @@ class TestRules(TestPrioDir): group=lxml.etree.Element("SEPort", name="6789/tcp"), children=lxml.etree.Element("Path", name="/etc/child-entries"), regex=lxml.etree.Element("Package", name="regex"), + replace_name=lxml.etree.Element("POSIXUser", name="regex"), slash=lxml.etree.Element("Path", name="/etc/trailing/slash"), no_slash=lxml.etree.Element("Path", name="/etc/no/trailing/slash/")) @@ -53,6 +54,8 @@ class TestRules(TestPrioDir): group="root", mode="0775"), regex=lxml.etree.Element("Package", name="regex", type="yum", version="any"), + replace_name=lxml.etree.Element("POSIXUser", name="regex", + home="/foobar%{bar}/regex"), slash=lxml.etree.Element("Path", name="/etc/trailing/slash", type="directory", owner="root", group="root", mode="0600"), @@ -70,6 +73,8 @@ class TestRules(TestPrioDir): in_file = copy.deepcopy(concrete) in_file['regex'].set("name", ".*") + in_file['replace_name'].set("home", "/foobar%{bar}/%{name}") + in_file['replace_name'].set("name", ".*") in_file['slash'].set("name", "/etc/trailing/slash/") in_file['no_slash'].set("name", "/etc/no/trailing/slash") @@ -91,6 +96,7 @@ class TestRules(TestPrioDir): rules3 = lxml.etree.Element("Rules", priority="10") rules3.append(in_file['duplicate']) rules3.append(in_file['regex']) + rules3.append(in_file['replace_name']) rules3.append(in_file['slash']) rules = {"rules1.xml": rules1, "rules2.xml": rules2, "rules3.xml": rules3} @@ -99,6 +105,7 @@ class TestRules(TestPrioDir): TestPrioDir.setUp(self) set_setup_default("lax_decryption", True) set_setup_default("rules_regex", False) + set_setup_default("rules_replace_name", False) def get_child(self, name): """ Turn one of the XML documents in `rules` into a child @@ -169,6 +176,17 @@ class TestRules(TestPrioDir): self._do_test('regex') Bcfg2.Options.setup.rules_regex = False + def test_replace_name(self): + """ Test that Rules handles replaces name in attribues with regular expressions """ + Bcfg2.Options.setup.rules_regex = False + Bcfg2.Options.setup.rules_replace_name = False + self._do_test_failure('replace_name', handles=False) + Bcfg2.Options.setup.rules_regex = True + Bcfg2.Options.setup.rules_replace_name = True + self._do_test('replace_name') + Bcfg2.Options.setup.rules_regex = False + Bcfg2.Options.setup.rules_replace_name = False + def test_slash(self): """ Test that Rules handles trailing slashes on Path entries """ self._do_test('slash') diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestTemplateHelper.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestTemplateHelper.py index 128d6cae5..1c2fa23a2 100644 --- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestTemplateHelper.py +++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestTemplateHelper.py @@ -2,6 +2,7 @@ import os import sys import Bcfg2.Server.Plugin from mock import Mock, MagicMock, patch +from Bcfg2.Utils import safe_module_name from Bcfg2.Server.Plugins.TemplateHelper import * # add all parent testsuite directories to sys.path to allow (most) @@ -39,8 +40,9 @@ class TestHelperModule(Bcfg2TestCase): mock_load_source.side_effect = ImportError attrs = dir(hm) hm.HandleEvent() - mock_load_source.assert_called_with(safe_module_name(hm._module_name), - hm.name) + mock_load_source.assert_called_with( + safe_module_name('TemplateHelper', hm._module_name), + hm.name) self.assertEqual(attrs, dir(hm)) self.assertEqual(hm._attrs, []) @@ -51,8 +53,9 @@ class TestHelperModule(Bcfg2TestCase): mock_load_source.return_value = Mock() attrs = dir(hm) hm.HandleEvent() - mock_load_source.assert_called_with(safe_module_name(hm._module_name), - hm.name) + mock_load_source.assert_called_with( + safe_module_name('TemplateHelper', hm._module_name), + hm.name) self.assertEqual(attrs, dir(hm)) self.assertEqual(hm._attrs, []) @@ -63,8 +66,9 @@ class TestHelperModule(Bcfg2TestCase): mock_load_source.return_value = module attrs = dir(hm) hm.HandleEvent() - mock_load_source.assert_called_with(safe_module_name(hm._module_name), - hm.name) + mock_load_source.assert_called_with( + safe_module_name('TemplateHelper', hm._module_name), + hm.name) self.assertEqual(attrs, dir(hm)) self.assertEqual(hm._attrs, []) @@ -74,8 +78,9 @@ class TestHelperModule(Bcfg2TestCase): mock_load_source.reset() mock_load_source.return_value = module hm.HandleEvent() - mock_load_source.assert_called_with(safe_module_name(hm._module_name), - hm.name) + mock_load_source.assert_called_with( + safe_module_name('TemplateHelper', hm._module_name), + hm.name) self.assertTrue(hasattr(hm, "foo")) self.assertTrue(hasattr(hm, "bar")) self.assertTrue(hasattr(hm, "baz")) @@ -87,8 +92,9 @@ class TestHelperModule(Bcfg2TestCase): mock_load_source.reset() mock_load_source.return_value = module hm.HandleEvent() - mock_load_source.assert_called_with(safe_module_name(hm._module_name), - hm.name) + mock_load_source.assert_called_with( + safe_module_name('TemplateHelper', hm._module_name), + hm.name) self.assertTrue(hasattr(hm, "foo")) self.assertTrue(hasattr(hm, "bar")) self.assertTrue(hasattr(hm, "quux")) diff --git a/testsuite/Testsrc/Testlib/TestUtils.py b/testsuite/Testsrc/Testlib/TestUtils.py index 4bed67248..a37f2ecbe 100644 --- a/testsuite/Testsrc/Testlib/TestUtils.py +++ b/testsuite/Testsrc/Testlib/TestUtils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import os import sys from Bcfg2.Utils import * @@ -42,3 +43,17 @@ class TestPackedDigitRange(Bcfg2TestCase): for test in exc: self.assertNotIn(test, rng) self.assertFalse(rng.includes(test)) + + +class TestIsString(Bcfg2TestCase): + def test_is_string(self): + for char in list(range(8)) + list(range(14, 32)): + self.assertFalse(is_string("foo" + chr(char) + "bar", 'UTF-8')) + for char in list(range(9, 14)) + list(range(33, 128)): + self.assertTrue(is_string("foo" + chr(char) + "bar", 'UTF-8')) + + ustr = 'é' + self.assertTrue(is_string(ustr, 'UTF-8')) + if not inPy3k: + self.assertFalse(is_string("foo" + chr(128) + "bar", 'ascii')) + self.assertFalse(is_string(ustr, 'ascii')) diff --git a/testsuite/Testsrc/test_code_checks.py b/testsuite/Testsrc/test_code_checks.py index ba4b19d1c..6d3c4c714 100644 --- a/testsuite/Testsrc/test_code_checks.py +++ b/testsuite/Testsrc/test_code_checks.py @@ -66,7 +66,6 @@ error_checks = { "VCS.py", "YUM24.py"], "lib/Bcfg2/Server/Plugins": ["Deps.py", - "Ldap.py", "Pkgmgr.py"] } diff --git a/testsuite/common.py b/testsuite/common.py index 4c7337e0d..45cfcbf0b 100644 --- a/testsuite/common.py +++ b/testsuite/common.py @@ -64,6 +64,7 @@ try: set_setup_default("time_zone") set_setup_default("web_debug", False) set_setup_default("web_prefix") + set_setup_default("django_settings") import Bcfg2.DBSettings Bcfg2.DBSettings.finalize_django_config() @@ -171,21 +172,22 @@ class Bcfg2TestCase(TestCase): msg = "XML trees are not equal: %s" else: msg += ": %s" - fullmsg = msg + "\nFirst: %s" % lxml.etree.tostring(el1) + \ + msg += "\n%s" + fullmsg = "First: %s" % lxml.etree.tostring(el1) + \ "\nSecond: %s" % lxml.etree.tostring(el2) - self.assertEqual(el1.tag, el2.tag, msg=fullmsg % "Tags differ") + self.assertEqual(el1.tag, el2.tag, msg=msg % ("Tags differ", fullmsg)) if el1.text is not None and el2.text is not None: self.assertEqual(el1.text.strip(), el2.text.strip(), - msg=fullmsg % "Text content differs") + msg=msg % ("Text content differs", fullmsg)) else: self.assertEqual(el1.text, el2.text, - msg=fullmsg % "Text content differs") + msg=msg % ("Text content differs", fullmsg)) self.assertItemsEqual(el1.attrib.items(), el2.attrib.items(), - msg=fullmsg % "Attributes differ") + msg=msg % ("Attributes differ", fullmsg)) self.assertEqual(len(el1.getchildren()), len(el2.getchildren()), - msg=fullmsg % "Different numbers of children") + msg=msg % ("Different numbers of children", fullmsg)) matched = [] for child1 in el1.getchildren(): for child2 in el2.xpath(child1.tag): @@ -199,10 +201,10 @@ class Bcfg2TestCase(TestCase): continue else: assert False, \ - fullmsg % ("Element %s is missing from second" % - lxml.etree.tostring(child1)) + msg % ("Element %s is missing from second" % + lxml.etree.tostring(child1), fullmsg) self.assertItemsEqual(el2.getchildren(), matched, - msg=fullmsg % "Second has extra element(s)") + msg=msg % ("Second has extra element(s)", fullmsg)) class DBModelTestCase(Bcfg2TestCase): diff --git a/testsuite/ext/ssl_protocols.py b/testsuite/ext/ssl_protocols.py new file mode 100644 index 000000000..66068d2a9 --- /dev/null +++ b/testsuite/ext/ssl_protocols.py @@ -0,0 +1,17 @@ +try: + from logilab.astng import MANAGER, scoped_nodes, node_classes + PYLINT=0 +except ImportError: + from astroid import MANAGER, scoped_nodes, node_classes + PYLINT=1 + +def ssl_transform(module): + if module.name == 'ssl': + for proto in ('SSLv23', 'TLSv1'): + module.locals['PROTOCOL_%s' % proto] = [node_classes.Const()] + +def register(linter): + if PYLINT == 0: + MANAGER.register_transformer(ssl_transform) + else: + MANAGER.register_transform(scoped_nodes.Module, ssl_transform) diff --git a/testsuite/pylintrc.conf b/testsuite/pylintrc.conf index 1d3ba8c88..50ece77db 100644 --- a/testsuite/pylintrc.conf +++ b/testsuite/pylintrc.conf @@ -19,7 +19,7 @@ persistent=no # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. -load-plugins=ext.exception_messages +load-plugins=ext.exception_messages,ext.ssl_protocols [MESSAGES CONTROL] diff --git a/testsuite/requirements.txt b/testsuite/requirements.txt index dce47c338..d67c64db6 100644 --- a/testsuite/requirements.txt +++ b/testsuite/requirements.txt @@ -4,6 +4,6 @@ mock sphinx pylint<0.29 pep8 -python-daemon +python-daemon<2.0.0 genshi argparse diff --git a/tools/upgrade/1.3/migrate_info.py b/tools/upgrade/1.3/migrate_info.py index 7f3bb9a29..2f8035bcd 100755 --- a/tools/upgrade/1.3/migrate_info.py +++ b/tools/upgrade/1.3/migrate_info.py @@ -48,9 +48,11 @@ def main(): parser.parse() for plugin in Bcfg2.Options.setup.plugins: - if plugin not in ['SSLCA', 'Cfg', 'TGenshi', 'TCheetah', 'SSHbase']: + plugin_name = plugin.__name__ + if plugin_name not in ['SSLCA', 'Cfg', 'TGenshi', 'TCheetah', + 'SSHbase']: continue - datastore = os.path.join(Bcfg2.Options.setup.repository, plugin) + datastore = os.path.join(Bcfg2.Options.setup.repository, plugin_name) for root, dirs, files in os.walk(datastore): for fname in files: if fname in [":info", "info"]: diff --git a/tools/upgrade/1.3/migrate_perms_to_mode.py b/tools/upgrade/1.3/migrate_perms_to_mode.py index 2dfb70388..c825c84d2 100755 --- a/tools/upgrade/1.3/migrate_perms_to_mode.py +++ b/tools/upgrade/1.3/migrate_perms_to_mode.py @@ -64,24 +64,26 @@ def main(): parser = Bcfg2.Options.get_parser( description="Migrate from Bcfg2 1.2 'perms' attribute to 1.3 'mode' " "attribute", - components=FileMonitor) + components=[FileMonitor]) parser.add_options([Bcfg2.Options.Common.repository, Bcfg2.Options.Common.plugins]) parser.parse() repo = Bcfg2.Options.setup.repository for plugin in Bcfg2.Options.setup.plugins: - if plugin in ['Base', 'Bundler', 'Rules']: - for root, _, files in os.walk(os.path.join(repo, plugin)): + plugin_name = plugin.__name__ + if plugin_name in ['Base', 'Bundler', 'Rules']: + for root, _, files in os.walk(os.path.join(repo, plugin_name)): if skip_path(root): continue for fname in files: if skip_path(fname): continue convertstructure(os.path.join(root, fname)) - if plugin not in ['Cfg', 'TGenshi', 'TCheetah', 'SSHbase', 'SSLCA']: + if plugin_name not in ['Cfg', 'TGenshi', 'TCheetah', 'SSHbase', + 'SSLCA']: continue - for root, dirs, files in os.walk(os.path.join(repo, plugin)): + for root, dirs, files in os.walk(os.path.join(repo, plugin_name)): if skip_path(root): continue for fname in files: |