summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/development/cfg.txt2
-rw-r--r--doc/releases/1.4.0pre1.txt4
-rw-r--r--doc/server/plugins/generators/cfg.txt47
-rw-r--r--doc/server/plugins/generators/examples/jinja2/simple.txt53
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/File.py44
-rw-r--r--src/lib/Bcfg2/Client/__init__.py29
-rw-r--r--src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html2
-rw-r--r--src/lib/Bcfg2/Reporting/templatetags/bcfg2_tags.py15
-rw-r--r--src/lib/Bcfg2/Server/Lint/Comments.py11
-rwxr-xr-xsrc/lib/Bcfg2/Server/Lint/Jinja2.py41
-rw-r--r--src/lib/Bcfg2/Server/Lint/TemplateAbuse.py8
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedJinja2Generator.py25
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgJinja2Generator.py52
-rw-r--r--testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py55
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgEncryptedJinja2Generator.py46
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgJinja2Generator.py47
-rw-r--r--testsuite/Testsrc/test_code_checks.py3
-rwxr-xr-xtestsuite/install.sh2
18 files changed, 382 insertions, 104 deletions
diff --git a/doc/development/cfg.txt b/doc/development/cfg.txt
index f93bb42c7..4e967368b 100644
--- a/doc/development/cfg.txt
+++ b/doc/development/cfg.txt
@@ -64,9 +64,11 @@ Generators
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgPlaintextGenerator.CfgPlaintextGenerator
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator.CfgGenshiGenerator
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgCheetahGenerator.CfgCheetahGenerator
+.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgJinja2Generator.CfgJinja2Generator
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenerator.CfgEncryptedGenerator
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenshiGenerator.CfgEncryptedGenshiGenerator
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgEncryptedCheetahGenerator.CfgEncryptedCheetahGenerator
+.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgEncryptedJinja2Generator.CfgEncryptedJinja2Generator
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgAuthorizedKeysGenerator.CfgAuthorizedKeysGenerator
Creators
diff --git a/doc/releases/1.4.0pre1.txt b/doc/releases/1.4.0pre1.txt
index 1f92f4665..779873f41 100644
--- a/doc/releases/1.4.0pre1.txt
+++ b/doc/releases/1.4.0pre1.txt
@@ -50,6 +50,10 @@ deprecated features (will be removed in a future release, likely 1.5)
* :ref:`server-plugins-structures-bundler`
* Deprecated use of an explicit name attribute
+
+ You can convert your existing bundles using
+ ``tools/upgrade/1.4/convert_bundles.py``.
+
* Deprecated :ref:`.genshi bundles
<server-plugins-structures-bundler-index-genshi-templates>` (use
.xml bundles and specify the genshi namespace instead)
diff --git a/doc/server/plugins/generators/cfg.txt b/doc/server/plugins/generators/cfg.txt
index 9ebbd2666..8b49e244b 100644
--- a/doc/server/plugins/generators/cfg.txt
+++ b/doc/server/plugins/generators/cfg.txt
@@ -30,8 +30,8 @@ in ``Cfg/etc/passwd/passwd``, while the ssh pam module config file,
``/etc/pam.d/sshd``, goes in ``Cfg/etc/pam.d/sshd/sshd``. The reason for
the like-name directory is to allow multiple versions of each file to
exist, as described below. Note that these files are exact copies of what
-will appear on the client machine (except when using Genshi or Cheetah
-templating -- see below).
+will appear on the client machine (except when using templates -- see
+below).
Group-Specific Files
====================
@@ -242,6 +242,27 @@ comment to appear in the final config file.::
# This is a comment in my template which will be stripped when it's processed through Cheetah
\# This comment will appear in the generated config file.
+.. _server-plugins-generators-cfg-jinja2:
+
+Jinja2 Templates
+-----------------
+
+Jinja2 templates allow you to use the `jinja2 templating system
+<http://jinja.pocoo.org/>`_. Jinja2 templates should be
+named with a ``.jinja2`` extension, e.g.::
+
+ % ls Cfg/etc/motd
+ info.xml motd.jinja2
+
+Examples
+~~~~~~~~
+
+.. toctree::
+ :glob:
+ :maxdepth: 1
+
+ examples/jinja2/*
+
Inside Templates
----------------
@@ -263,10 +284,10 @@ Several variables are pre-defined inside templates:
| repo | The path to the Bcfg2 repository on the filesystem |
+-------------+--------------------------------------------------------+
| path | In Genshi templates, ``path`` is a synonym for |
-| | ``source_path``. In Cheetah templates, it's a synonym |
-| | for ``name``. For this reason, use of ``path`` is |
-| | discouraged, and it may be deprecated in a future |
-| | release. |
+| | ``source_path``. In Cheetah templates and Jinja2 |
+| | templates, it's a synonym for ``name``. For this |
+| | reason, use of ``path`` is discouraged, and it may be |
+| | deprecated in a future release. |
+-------------+--------------------------------------------------------+
To access these variables in a Genshi template, you can simply use the
@@ -274,6 +295,10 @@ name, e.g.::
Path to this file: ${name}
+Similarly, in a Jinja2 template::
+
+ Path to this file: {{ name }}
+
In a Cheetah template, the variables are properties of ``self``,
e.g.::
@@ -283,15 +308,15 @@ Notes on Using Templates
------------------------
Templates can be host and group specific as well. Deltas will not be
-processed for any Genshi or Cheetah base file.
+processed for any Genshi, Cheetah, or Jinja2 base file.
.. note::
If you are using templating in combination with host-specific
or group-specific files, you will need to ensure that the ``.genshi``
- or ``.cheetah`` extension is at the **end** of the filename. Using the
- examples from above for *host.example.com* and group *server* you would
- have the following::
+ ``.cheetah`` or ``.jinja2`` extension is at the **end** of the filename.
+ Using the examples from above for *host.example.com* and group *server*
+ you would have the following::
Cfg/etc/fstab/fstab.H_host.example.com.genshi
Cfg/etc/fstab/fstab.G50_server.cheetah
@@ -345,7 +370,7 @@ An encrypted file should end with ``.crypt``, e.g.::
Cfg/etc/foo.conf/foo.conf.crypt
Cfg/etc/foo.conf/foo.conf.G10_foo.crypt
-Encrypted Genshi or Cheetah templates can have the extensions in
+Encrypted Genshi, Cheetah, and Jinja2 templates can have the extensions in
either order, e.g.::
Cfg/etc/foo.conf/foo.conf.crypt.genshi
diff --git a/doc/server/plugins/generators/examples/jinja2/simple.txt b/doc/server/plugins/generators/examples/jinja2/simple.txt
new file mode 100644
index 000000000..b4ab844fb
--- /dev/null
+++ b/doc/server/plugins/generators/examples/jinja2/simple.txt
@@ -0,0 +1,53 @@
+.. -*- mode: rst -*-
+
+=========================
+ Basic Jinja2 Templates
+=========================
+
+This simple example demonstrates basic usage of Jinja2 templates.
+
+``/var/lib/bcfg2/Cfg/foo/foo.jinja2``
+
+.. code-block:: none
+
+ Hostname is {{ metadata.hostname }}
+ Filename is {{ name }}
+ Template is {{ source_path }}
+ Groups:
+ {% for group in metadata.groups -%}
+ * {{ group }}
+ {% endfor %}
+ Categories:
+ {% for category in metadata.categories -%}
+ * {{ category }} -- {{ metadata.categories[category] }}
+ {% endfor %}
+
+ Probes:
+ {% for probe in metadata.Probes -%}
+ * {{ probe }} -- {{ metadata.Probes[probe] }}
+ {% endfor %}
+
+Output
+======
+
+.. code-block:: xml
+
+ <Path type="file" name="/foo" owner="root" mode="0644" group="root">
+ Hostname is topaz.mcs.anl.gov
+ Filename is /foo
+ Template is /var/lib/bcfg2/Cfg/foo/foo.jinja2
+ Groups:
+ * desktop
+ * mcs-base
+ * ypbound
+ * workstation
+ * xserver
+ * debian-sarge
+ * debian
+ * a
+ Categories:
+ * test -- a
+
+ Probes:
+ * os -- debian
+ </Path>
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/File.py b/src/lib/Bcfg2/Client/Tools/POSIX/File.py
index d7a70e202..0452ea258 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/File.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/File.py
@@ -3,7 +3,6 @@
import os
import sys
import stat
-import time
import difflib
import tempfile
import Bcfg2.Options
@@ -189,12 +188,11 @@ class POSIXFile(POSIXTool):
prompt.append('Binary file, no printable diff')
attrs['current_bfile'] = b64encode(content)
else:
+ diff = self._diff(content, self._get_data(entry)[0],
+ filename=entry.get("name"))
if interactive:
- diff = self._diff(content, self._get_data(entry)[0],
- difflib.unified_diff,
- filename=entry.get("name"))
if diff:
- udiff = '\n'.join(l.rstrip('\n') for l in diff)
+ udiff = '\n'.join(diff)
if hasattr(udiff, "decode"):
udiff = udiff.decode(Bcfg2.Options.setup.encoding)
try:
@@ -209,8 +207,6 @@ class POSIXFile(POSIXTool):
prompt.append("Diff took too long to compute, no "
"printable diff")
if not sensitive:
- diff = self._diff(content, self._get_data(entry)[0],
- difflib.ndiff, filename=entry.get("name"))
if diff:
attrs["current_bdiff"] = b64encode("\n".join(diff))
else:
@@ -221,28 +217,12 @@ class POSIXFile(POSIXTool):
for attr, val in attrs.items():
entry.set(attr, val)
- def _diff(self, content1, content2, difffunc, filename=None):
- """ Return a diff of the two strings, as produced by difffunc.
- warns after 5 seconds and times out after 30 seconds. """
- rv = []
- start = time.time()
- longtime = False
- for diffline in difffunc(content1.split('\n'),
- content2.split('\n')):
- now = time.time()
- rv.append(diffline)
- if now - start > 5 and not longtime:
- if filename:
- self.logger.info("POSIX: Diff of %s taking a long time" %
- filename)
- else:
- self.logger.info("POSIX: Diff taking a long time")
- longtime = True
- elif now - start > 30:
- if filename:
- self.logger.error("POSIX: Diff of %s took too long; "
- "giving up" % filename)
- else:
- self.logger.error("POSIX: Diff took too long; giving up")
- return False
- return rv
+ def _diff(self, content1, content2, filename=None):
+ """ Return a unified diff of the two strings """
+
+ fromfile = "%s (on disk)" % filename if filename else ""
+ tofile = "%s (from bcfg2)" % filename if filename else ""
+ return difflib.unified_diff(content1.split('\n'),
+ content2.split('\n'),
+ fromfile=fromfile,
+ tofile=tofile)
diff --git a/src/lib/Bcfg2/Client/__init__.py b/src/lib/Bcfg2/Client/__init__.py
index 346444b2c..073aa7694 100644
--- a/src/lib/Bcfg2/Client/__init__.py
+++ b/src/lib/Bcfg2/Client/__init__.py
@@ -144,7 +144,10 @@ class Client(object):
Bcfg2.Options.BooleanOption(
"-e", "--show-extra", help='Enable extra entry output'),
Bcfg2.Options.BooleanOption(
- "-k", "--kevlar", help='Run in bulletproof mode')]
+ "-k", "--kevlar", help='Run in bulletproof mode'),
+ Bcfg2.Options.BooleanOption(
+ "-i", "--only-important",
+ help='Only configure the important entries')]
def __init__(self):
self.config = None
@@ -559,7 +562,9 @@ class Client(object):
if x not in b_to_rem]
# take care of important entries first
- if not Bcfg2.Options.setup.dry_run:
+ if (not Bcfg2.Options.setup.dry_run or
+ Bcfg2.Options.setup.only_important):
+ important_installs = set()
for parent in self.config.findall(".//Path/.."):
name = parent.get("name")
if not name or (name in Bcfg2.Options.setup.except_bundles and
@@ -574,6 +579,9 @@ class Client(object):
if t.handlesEntry(cfile) and t.canVerify(cfile)]
if not tools:
continue
+ if Bcfg2.Options.setup.dry_run:
+ important_installs.add(cfile)
+ continue
if (Bcfg2.Options.setup.interactive and not
self.promptFilter("Install %s: %s? (y/N):",
[cfile])):
@@ -589,6 +597,11 @@ class Client(object):
cfile.set('qtext', '')
if tools[0].VerifyPath(cfile, []):
self.whitelist.remove(cfile)
+ if Bcfg2.Options.setup.dry_run and len(important_installs) > 0:
+ self.logger.info("In dryrun mode: "
+ "suppressing entry installation for:")
+ self.logger.info(["%s:%s" % (e.tag, e.get('name'))
+ for e in important_installs])
def Inventory(self):
"""
@@ -845,11 +858,13 @@ class Client(object):
self.times['inventory'] = time.time()
self.CondDisplayState('initial')
self.InstallImportant()
- self.Decide()
- self.Install()
- self.times['install'] = time.time()
- self.Remove()
- self.times['remove'] = time.time()
+ if not Bcfg2.Options.setup.only_important:
+ self.Decide()
+ self.Install()
+ self.times['install'] = time.time()
+ self.Remove()
+ self.times['remove'] = time.time()
+
if self.modified:
self.ReInventory()
self.times['reinventory'] = time.time()
diff --git a/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html b/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html
index 33c78a5f0..6a314bd88 100644
--- a/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html
+++ b/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html
@@ -32,7 +32,7 @@ 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.timestamp|isstale:entry_max %}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 %}>{{ entry.timestamp|date:"Y-m-d\&\n\b\s\p\;H:i"|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/templatetags/bcfg2_tags.py b/src/lib/Bcfg2/Reporting/templatetags/bcfg2_tags.py
index ceb9f5d91..4a93e77e0 100644
--- a/src/lib/Bcfg2/Reporting/templatetags/bcfg2_tags.py
+++ b/src/lib/Bcfg2/Reporting/templatetags/bcfg2_tags.py
@@ -189,19 +189,6 @@ def build_metric_list(mdict):
@register.filter
-def isstale(timestamp, entry_max=None):
- """
- Check for a stale timestamp
-
- Compares two timestamps and returns True if the
- difference is greater then 24 hours.
- """
- if not entry_max:
- entry_max = datetime.now()
- return entry_max - timestamp > timedelta(hours=24)
-
-
-@register.filter
def sort_interactions_by_name(value):
"""
Sort an interaction list by client name
@@ -318,7 +305,7 @@ def determine_client_state(entry):
dirty. If the client is reporting dirty, this will figure out just
_how_ dirty and adjust the color accordingly.
"""
- if isstale(entry.timestamp):
+ if entry.isstale():
return "stale-lineitem"
if entry.state == 'clean':
if entry.extra_count > 0:
diff --git a/src/lib/Bcfg2/Server/Lint/Comments.py b/src/lib/Bcfg2/Server/Lint/Comments.py
index e2d1ec597..fc4506c12 100644
--- a/src/lib/Bcfg2/Server/Lint/Comments.py
+++ b/src/lib/Bcfg2/Server/Lint/Comments.py
@@ -9,6 +9,7 @@ from Bcfg2.Server.Plugins.Cfg.CfgPlaintextGenerator \
import CfgPlaintextGenerator
from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator
from Bcfg2.Server.Plugins.Cfg.CfgCheetahGenerator import CfgCheetahGenerator
+from Bcfg2.Server.Plugins.Cfg.CfgJinja2Generator import CfgJinja2Generator
from Bcfg2.Server.Plugins.Cfg.CfgInfoXML import CfgInfoXML
@@ -76,6 +77,14 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin):
type=Bcfg2.Options.Types.comma_list, default=[],
help="Required comments for Cheetah-templated Cfg files"),
Bcfg2.Options.Option(
+ cf=("Comments", "jinja2_keywords"),
+ type=Bcfg2.Options.Types.comma_list, default=[],
+ help="Required keywords for Jinja2-templated Cfg files"),
+ Bcfg2.Options.Option(
+ cf=("Comments", "jinja2_comments"),
+ type=Bcfg2.Options.Types.comma_list, default=[],
+ help="Required comments for Jinja2-templated Cfg files"),
+ Bcfg2.Options.Option(
cf=("Comments", "infoxml_keywords"),
type=Bcfg2.Options.Types.comma_list, default=[],
help="Required keywords for info.xml files"),
@@ -235,6 +244,8 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin):
rtype = "cfg"
elif isinstance(entry, CfgCheetahGenerator):
rtype = "cheetah"
+ elif isinstance(entry, CfgJinja2Generator):
+ rtype = "jinja2"
elif isinstance(entry, CfgInfoXML):
self.check_xml(entry.infoxml.name,
entry.infoxml.pnode.data,
diff --git a/src/lib/Bcfg2/Server/Lint/Jinja2.py b/src/lib/Bcfg2/Server/Lint/Jinja2.py
new file mode 100755
index 000000000..333249cc2
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/Jinja2.py
@@ -0,0 +1,41 @@
+""" Check Jinja2 templates for syntax errors. """
+
+import sys
+import Bcfg2.Server.Lint
+from jinja2 import Template, TemplateSyntaxError
+from Bcfg2.Server.Plugins.Cfg.CfgJinja2Generator import CfgJinja2Generator
+
+
+class Jinja2(Bcfg2.Server.Lint.ServerPlugin):
+ """ Check Jinja2 templates for syntax errors. """
+
+ def Run(self):
+ if 'Cfg' in self.core.plugins:
+ self.check_cfg()
+
+ @classmethod
+ def Errors(cls):
+ return {"jinja2-syntax-error": "error",
+ "unknown-jinja2-error": "error"}
+
+ def check_template(self, entry):
+ """ Generic check for all jinja2 templates """
+ try:
+ Template(entry.data.decode(entry.encoding))
+ except TemplateSyntaxError:
+ err = sys.exc_info()[1]
+ self.LintError("jinja2-syntax-error",
+ "Jinja2 syntax error in %s: %s" % (entry.name, err))
+ except:
+ err = sys.exc_info()[1]
+ self.LintError("unknown-jinja2-error",
+ "Unknown Jinja2 error in %s: %s" % (entry.name,
+ err))
+
+ def check_cfg(self):
+ """ Check jinja2 templates in Cfg for syntax errors. """
+ for entryset in self.core.plugins['Cfg'].entries.values():
+ for entry in entryset.entries.values():
+ if (self.HandlesFile(entry.name) and
+ isinstance(entry, CfgJinja2Generator)):
+ self.check_template(entry)
diff --git a/src/lib/Bcfg2/Server/Lint/TemplateAbuse.py b/src/lib/Bcfg2/Server/Lint/TemplateAbuse.py
index 202a1487d..5a80a5884 100644
--- a/src/lib/Bcfg2/Server/Lint/TemplateAbuse.py
+++ b/src/lib/Bcfg2/Server/Lint/TemplateAbuse.py
@@ -8,16 +8,20 @@ from Bcfg2.Server.Plugin import default_path_metadata
from Bcfg2.Server.Plugins.Cfg.CfgInfoXML import CfgInfoXML
from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator
from Bcfg2.Server.Plugins.Cfg.CfgCheetahGenerator import CfgCheetahGenerator
+from Bcfg2.Server.Plugins.Cfg.CfgJinja2Generator import CfgJinja2Generator
from Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenshiGenerator import \
CfgEncryptedGenshiGenerator
from Bcfg2.Server.Plugins.Cfg.CfgEncryptedCheetahGenerator import \
CfgEncryptedCheetahGenerator
+from Bcfg2.Server.Plugins.Cfg.CfgEncryptedJinja2Generator import \
+ CfgEncryptedJinja2Generator
class TemplateAbuse(Bcfg2.Server.Lint.ServerPlugin):
""" Check for templated scripts or executables. """
- templates = [CfgGenshiGenerator, CfgCheetahGenerator,
- CfgEncryptedGenshiGenerator, CfgEncryptedCheetahGenerator]
+ templates = [CfgGenshiGenerator, CfgCheetahGenerator, CfgJinja2Generator,
+ CfgEncryptedGenshiGenerator, CfgEncryptedCheetahGenerator,
+ CfgEncryptedJinja2Generator]
extensions = [".pl", ".py", ".sh", ".rb"]
def Run(self):
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedJinja2Generator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedJinja2Generator.py
new file mode 100644
index 000000000..c8da84ae0
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedJinja2Generator.py
@@ -0,0 +1,25 @@
+""" Handle encrypted Jinja2 templates (.crypt.jinja2 or
+.jinja2.crypt files)"""
+
+from Bcfg2.Server.Plugins.Cfg.CfgJinja2Generator import CfgJinja2Generator
+from Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenerator \
+ import CfgEncryptedGenerator
+
+
+class CfgEncryptedJinja2Generator(CfgJinja2Generator, CfgEncryptedGenerator):
+ """ CfgEncryptedJinja2Generator lets you encrypt your Jinja2
+ :ref:`server-plugins-generators-cfg` files on the server """
+
+ #: handle .crypt.jinja2 or .jinja2.crypt files
+ __extensions__ = ['jinja2.crypt', 'crypt.jinja2']
+
+ #: Override low priority from parent class
+ __priority__ = 0
+
+ def handle_event(self, event):
+ CfgEncryptedGenerator.handle_event(self, event)
+ handle_event.__doc__ = CfgEncryptedGenerator.handle_event.__doc__
+
+ def get_data(self, entry, metadata):
+ return CfgJinja2Generator.get_data(self, entry, metadata)
+ get_data.__doc__ = CfgJinja2Generator.get_data.__doc__
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgJinja2Generator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgJinja2Generator.py
new file mode 100644
index 000000000..e36ee78aa
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgJinja2Generator.py
@@ -0,0 +1,52 @@
+""" The CfgJinja2Generator allows you to use the `Jinja2
+<http://jinja.pocoo.org/>`_ templating system to generate
+:ref:`server-plugins-generators-cfg` files. """
+
+import Bcfg2.Options
+from Bcfg2.Server.Plugin import PluginExecutionError, \
+ DefaultTemplateDataProvider, get_template_data
+from Bcfg2.Server.Plugins.Cfg import CfgGenerator
+
+try:
+ from jinja2 import Template
+ HAS_JINJA2 = True
+except ImportError:
+ HAS_JINJA2 = False
+
+
+class DefaultJinja2DataProvider(DefaultTemplateDataProvider):
+ """ Template data provider for Jinja2 templates. Jinja2 and
+ Genshi currently differ over the value of the ``path`` variable,
+ which is why this is necessary. """
+
+ def get_template_data(self, entry, metadata, template):
+ rv = DefaultTemplateDataProvider.get_template_data(self, entry,
+ metadata, template)
+ rv['path'] = rv['name']
+ return rv
+
+
+class CfgJinja2Generator(CfgGenerator):
+ """ The CfgJinja2Generator allows you to use the `Jinja2
+ <http://jinja.pocoo.org/>`_ templating system to generate
+ :ref:`server-plugins-generators-cfg` files. """
+
+ #: Handle .jinja2 files
+ __extensions__ = ['jinja2']
+
+ #: Low priority to avoid matching host- or group-specific
+ #: .crypt.jinja2 files
+ __priority__ = 50
+
+ def __init__(self, fname, spec):
+ CfgGenerator.__init__(self, fname, spec)
+ if not HAS_JINJA2:
+ raise PluginExecutionError("Jinja2 is not available")
+ __init__.__doc__ = CfgGenerator.__init__.__doc__
+
+ def get_data(self, entry, metadata):
+ template = Template(self.data.decode(Bcfg2.Options.setup.encoding))
+ return template.render(
+ get_template_data(entry, metadata, self.name,
+ default=DefaultJinja2DataProvider()))
+ get_data.__doc__ = CfgGenerator.get_data.__doc__
diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py
index 31e297888..69dd562be 100644
--- a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py
+++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py
@@ -270,7 +270,6 @@ class TestPOSIXFile(TestPOSIXTool):
mock_open.assert_called_with(entry.get("name"))
mock_open.return_value.read.assert_any_call()
ptool._diff.assert_called_with(ondisk, entry.text,
- difflib.unified_diff,
filename=entry.get("name"))
self.assertIsNotNone(entry.get("qtext"))
del entry.attrib['qtext']
@@ -280,8 +279,8 @@ class TestPOSIXFile(TestPOSIXTool):
entry = reset()
ptool._get_diffs(entry, content=ondisk)
self.assertFalse(mock_open.called)
- ptool._diff.assert_called_with(ondisk, entry.text, difflib.ndiff,
- filename=entry.get("name"))
+ ptool._diff.assert_called_with(ondisk, entry.text,
+ filename=entry.get("name"))
self.assertIsNone(entry.get("qtext"))
self.assertEqual(entry.get("current_bdiff"),
b64encode("\n".join(ptool._diff.return_value)))
@@ -296,9 +295,7 @@ class TestPOSIXFile(TestPOSIXTool):
mock_open.assert_called_with(entry.get("name"))
mock_open.return_value.read.assert_any_call()
self.assertItemsEqual(ptool._diff.call_args_list,
- [call(ondisk, entry.text, difflib.unified_diff,
- filename=entry.get("name")),
- call(ondisk, entry.text, difflib.ndiff,
+ [call(ondisk, entry.text,
filename=entry.get("name"))])
self.assertIsNotNone(entry.get("qtext"))
self.assertTrue(entry.get("qtext").startswith("test\n"))
@@ -318,9 +315,7 @@ class TestPOSIXFile(TestPOSIXTool):
mock_open.assert_called_with(entry.get("name"))
mock_open.return_value.read.assert_any_call()
self.assertItemsEqual(ptool._diff.call_args_list,
- [call(ondisk, encoded, difflib.unified_diff,
- filename=entry.get("name")),
- call(ondisk, encoded, difflib.ndiff,
+ [call(ondisk, encoded,
filename=entry.get("name"))])
self.assertIsNotNone(entry.get("qtext"))
self.assertEqual(entry.get("current_bdiff"),
@@ -415,35 +410,23 @@ class TestPOSIXFile(TestPOSIXTool):
ptool._rename_tmpfile.assert_called_with(newfile, entry)
mock_install.assert_called_with(ptool, entry)
- @patch("time.time")
- def test_diff(self, mock_time):
+ @patch("difflib.unified_diff")
+ def test_diff(self, mock_diff):
ptool = self.get_obj()
+ filename = "/test"
content1 = "line1\nline2"
content2 = "line3"
- self.now = 1345640723
-
- def time_rv():
- self.now += 1
- return self.now
- mock_time.side_effect = time_rv
-
rv = ["line1", "line2", "line3"]
- func = Mock()
- func.return_value = rv
- self.assertItemsEqual(ptool._diff(content1, content2, func), rv)
- func.assert_called_with(["line1", "line2"], ["line3"])
-
- func.reset_mock()
- mock_time.reset_mock()
- def time_rv():
- self.now += 5
- return self.now
- mock_time.side_effect = time_rv
-
- def slow_diff(content1, content2):
- for i in range(1, 10):
- yield "line%s" % i
- func.side_effect = slow_diff
- self.assertFalse(ptool._diff(content1, content2, func), rv)
- func.assert_called_with(["line1", "line2"], ["line3"])
+ mock_diff.return_value = rv
+ self.assertItemsEqual(ptool._diff(content1, content2), rv)
+ mock_diff.assert_called_with(["line1", "line2"], ["line3"],
+ fromfile='', tofile='')
+
+ mock_diff.reset_mock()
+ self.assertItemsEqual(ptool._diff(content1, content2,
+ filename=filename),
+ rv)
+ mock_diff.assert_called_with(["line1", "line2"], ["line3"],
+ fromfile='/test (on disk)',
+ tofile='/test (from bcfg2)')
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgEncryptedJinja2Generator.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgEncryptedJinja2Generator.py
new file mode 100644
index 000000000..6857f933b
--- /dev/null
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgEncryptedJinja2Generator.py
@@ -0,0 +1,46 @@
+import os
+import sys
+from Bcfg2.Server.Plugins.Cfg.CfgEncryptedJinja2Generator import *
+
+# add all parent testsuite directories to sys.path to allow (most)
+# relative imports in python 2.4
+path = os.path.dirname(__file__)
+while path != "/":
+ if os.path.basename(path).lower().startswith("test"):
+ sys.path.append(path)
+ if os.path.basename(path) == "testsuite":
+ break
+ path = os.path.dirname(path)
+from common import *
+
+try:
+ from TestServer.TestPlugins.TestCfg.TestCfgJinja2Generator import \
+ TestCfgJinja2Generator
+ from Bcfg2.Server.Plugins.Cfg.CfgJinja2Generator import HAS_JINJA2
+except ImportError:
+ TestCfgJinja2Generator = object
+ HAS_JINJA2 = False
+
+try:
+ from TestServer.TestPlugins.TestCfg.TestCfgEncryptedGenerator import \
+ TestCfgEncryptedGenerator
+ from Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenerator import HAS_CRYPTO
+except ImportError:
+ TestCfgEncryptedGenerator = object
+ HAS_CRYPTO = False
+
+
+class TestCfgEncryptedJinja2Generator(TestCfgJinja2Generator,
+ TestCfgEncryptedGenerator):
+ test_obj = CfgEncryptedJinja2Generator
+
+ @skipUnless(HAS_CRYPTO, "Encryption libraries not found, skipping")
+ @skipUnless(HAS_JINJA2, "Jinja2 libraries not found, skipping")
+ def setUp(self):
+ pass
+
+ def test_handle_event(self):
+ TestCfgEncryptedGenerator.test_handle_event(self)
+
+ def test_get_data(self):
+ TestCfgJinja2Generator.test_get_data(self)
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgJinja2Generator.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgJinja2Generator.py
new file mode 100644
index 000000000..036380d56
--- /dev/null
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgJinja2Generator.py
@@ -0,0 +1,47 @@
+import os
+import sys
+import lxml.etree
+from mock import Mock, MagicMock, patch
+from Bcfg2.Server.Plugins.Cfg.CfgJinja2Generator import *
+
+# add all parent testsuite directories to sys.path to allow (most)
+# relative imports in python 2.4
+path = os.path.dirname(__file__)
+while path != "/":
+ if os.path.basename(path).lower().startswith("test"):
+ sys.path.append(path)
+ if os.path.basename(path) == "testsuite":
+ break
+ path = os.path.dirname(path)
+from common import *
+from TestServer.TestPlugins.TestCfg.Test_init import TestCfgGenerator
+
+
+class TestCfgJinja2Generator(TestCfgGenerator):
+ test_obj = CfgJinja2Generator
+
+ @skipUnless(HAS_JINJA2, "Jinja2 libraries not found, skipping")
+ def setUp(self):
+ TestCfgGenerator.setUp(self)
+ set_setup_default("repository", datastore)
+
+ @patch("Bcfg2.Server.Plugins.Cfg.CfgJinja2Generator.Template")
+ @patch("Bcfg2.Server.Plugins.Cfg.CfgJinja2Generator.get_template_data")
+ def test_get_data(self, mock_get_template_data, mock_Template):
+ ccg = self.get_obj()
+ ccg.data = "data"
+ entry = lxml.etree.Element("Path", name="/test.txt")
+ metadata = Mock()
+
+ template_vars = dict(name=entry.get("name"),
+ metadata=metadata,
+ path=ccg.name,
+ source_path=ccg.name,
+ repo=datastore)
+ mock_get_template_data.return_value = template_vars
+
+ self.assertEqual(ccg.get_data(entry, metadata),
+ mock_Template.return_value.render.return_value)
+ mock_Template.assert_called_with("data".decode(Bcfg2.Options.setup.encoding))
+ tmpl = mock_Template.return_value
+ tmpl.render.assert_called_with(template_vars)
diff --git a/testsuite/Testsrc/test_code_checks.py b/testsuite/Testsrc/test_code_checks.py
index 2b8b05926..77b170809 100644
--- a/testsuite/Testsrc/test_code_checks.py
+++ b/testsuite/Testsrc/test_code_checks.py
@@ -38,6 +38,7 @@ contingent_checks = {
("yum",): {"lib/Bcfg2/Client/Tools": ["YUM.py"]},
("genshi",): {"lib/Bcfg2/Server/Plugins/Cfg": ["CfgGenshiGenerator.py"]},
("Cheetah",): {"lib/Bcfg2/Server/Plugins/Cfg": ["CfgCheetahGenerator.py"]},
+ ("jinja2",): {"lib/Bcfg2/Server/Plugins/Cfg": ["CfgJinja2Generator.py"]},
("M2Crypto",): {"lib/Bcfg2": ["Encryption.py"],
"lib/Bcfg2/Server/Plugins/Cfg":
["CfgEncryptedGenerator.py"]},
@@ -45,6 +46,8 @@ contingent_checks = {
["CfgEncryptedGenshiGenerator.py"]},
("M2Crypto", "Cheetah"): {"lib/Bcfg2/Server/Plugins/Cfg":
["CfgEncryptedCheetahGenerator.py"]},
+ ("M2Crypto", "jinja2"): {"lib/Bcfg2/Server/Plugins/Cfg":
+ ["CfgEncryptedJinja2Generator.py"]},
}
# perform only error checking on the listed files
diff --git a/testsuite/install.sh b/testsuite/install.sh
index 50b91a4d2..6f36d4bef 100755
--- a/testsuite/install.sh
+++ b/testsuite/install.sh
@@ -11,7 +11,7 @@ if [[ ${PYVER:0:1} == "2" && $PYVER != "2.7" ]]; then
fi
if [[ "$WITH_OPTIONAL_DEPS" == "yes" ]]; then
- pip install --use-mirrors PyYAML pyinotify boto pylibacl 'django<1.5'
+ pip install --use-mirrors PyYAML pyinotify boto pylibacl 'django<1.5' Jinja2
easy_install https://fedorahosted.org/released/python-augeas/python-augeas-0.4.1.tar.gz
if [[ ${PYVER:0:1} == "2" ]]; then
# django supports py3k, but South doesn't, and the django bits