diff options
26 files changed, 415 insertions, 130 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/getting_started/index.txt b/doc/getting_started/index.txt index 62e804553..f619447e2 100644 --- a/doc/getting_started/index.txt +++ b/doc/getting_started/index.txt @@ -255,6 +255,10 @@ Once you have the server setup, you may be interested in Platform-specific Quickstart Notes ================================== -* :ref:`appendix-guides-centos` -* :ref:`appendix-guides-ubuntu` -* :ref:`getting_started-macosx-notes` +.. toctree:: + :maxdepth: 1 + + CentOS </appendix/guides/centos> + Ubuntu </appendix/guides/ubuntu> + Gentoo </appendix/guides/gentoo> + Mac OS X <macosx/notes> diff --git a/doc/man/bcfg2-server.txt b/doc/man/bcfg2-server.txt index f85964ae7..33d0df6cf 100644 --- a/doc/man/bcfg2-server.txt +++ b/doc/man/bcfg2-server.txt @@ -22,18 +22,18 @@ configurations to clients based on the data in its repository. Options ------- --C configfile Specify alternate bcfg2.conf location. --D pidfile Daemonize, placing the program pid in *pidfile*. --E encoding Specify the encoding of config files. --Q path Specify the path to the server repository. --S server Manually specify the server location (as opposed to - using the value in bcfg2.conf). This should be in - the format "https://server:port" --d Enable debugging output. --v Run in verbose mode. --h Print usage information. ---ssl-key=key Specify the path to the SSL key. ---no-fam-blocking Synonym for fam_blocking = False in bcfg2.conf +-C configfile Specify alternate bcfg2.conf location. +-D pidfile Daemonize, placing the program pid in *pidfile*. +-E encoding Specify the encoding of config files. +-Q path Specify the path to the server repository. +-S server Manually specify the server location (as opposed to + using the value in bcfg2.conf). This should be in + the format "https://server:port" +-d Enable debugging output. +-v Run in verbose mode. +-h Print usage information. +--ssl-key=key Specify the path to the SSL key. +--no-fam-blocking Synonym for fam_blocking = False in bcfg2.conf See Also -------- 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/releases/index.txt b/doc/releases/index.txt index 42a2306f6..479aa19de 100644 --- a/doc/releases/index.txt +++ b/doc/releases/index.txt @@ -7,4 +7,7 @@ Release Announcements ===================== -.. include:: 1.3.4.txt +.. toctree:: + + 1.4.0pre1 + 1.3.4 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/doc/server/plugins/probes/index.txt b/doc/server/plugins/probes/index.txt index 091c85e63..434ce20a8 100644 --- a/doc/server/plugins/probes/index.txt +++ b/doc/server/plugins/probes/index.txt @@ -15,7 +15,7 @@ generate an `/etc/auto.master` autofs config file for each type. Here we will look at how to do this. Probes also allow dynamic group assignment for clients, see -:ref:`_server-plugins-probes-dynamic-groups`. +:ref:`server-plugins-probes-dynamic-groups`. First, create a ``Probes`` directory in our toplevel repository location:: diff --git a/schemas/acl.xsd b/schemas/acl.xsd index 0c3e3ecdd..ac678b6c1 100644 --- a/schemas/acl.xsd +++ b/schemas/acl.xsd @@ -3,7 +3,7 @@ <xsd:annotation> <xsd:documentation> acl config schema for bcfg2 - Matt Schwager + Matt Schwager </xsd:documentation> </xsd:annotation> diff --git a/schemas/types.xsd b/schemas/types.xsd index a0fb7ed0a..0a55f6355 100644 --- a/schemas/types.xsd +++ b/schemas/types.xsd @@ -487,9 +487,9 @@ <xsd:annotation> <xsd:documentation> This field is typically used to record general information - about the account or its user(s) such as their real name - and phone number. If this is not set, the GECOS will be - the same as the username. + about the account or its user(s) such as their real name + and phone number. If this is not set, the GECOS will be + the same as the username. </xsd:documentation> </xsd:annotation> </xsd:attribute> 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/src/lib/Bcfg2/Server/Plugins/Decisions.py b/src/lib/Bcfg2/Server/Plugins/Decisions.py index 3d3ef8f8c..b30a9acea 100644 --- a/src/lib/Bcfg2/Server/Plugins/Decisions.py +++ b/src/lib/Bcfg2/Server/Plugins/Decisions.py @@ -31,4 +31,4 @@ class Decisions(Bcfg2.Server.Plugin.Plugin, self.blacklist = DecisionFile(os.path.join(self.data, "blacklist.xml")) def GetDecisions(self, metadata, mode): - return getattr(self, mode).get_decision(metadata) + return getattr(self, mode).get_decisions(metadata) 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/Testlib/TestServer/TestPlugins/TestDecisions.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestDecisions.py index 537ceb4ff..8b4df8abb 100644 --- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestDecisions.py +++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestDecisions.py @@ -52,9 +52,9 @@ class TestDecisions(TestPlugin, TestDecision): metadata = Mock() self.assertEqual(d.GetDecisions(metadata, "whitelist"), - d.whitelist.get_decision.return_value) - d.whitelist.get_decision.assert_called_with(metadata) + d.whitelist.get_decisions.return_value) + d.whitelist.get_decisions.assert_called_with(metadata) self.assertEqual(d.GetDecisions(metadata, "blacklist"), - d.blacklist.get_decision.return_value) - d.blacklist.get_decision.assert_called_with(metadata) + d.blacklist.get_decisions.return_value) + d.blacklist.get_decisions.assert_called_with(metadata) 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 |