diff --git a/ChangeLog b/ChangeLog index 77dae562..f11f2c73 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,8 @@ - doc: fix user-groups doc to reference plural ssh-authorized-keys (LP: #1327065) [Joern Heissler] - fix 'make test' in python 2.6 + - support jinja2 as a templating engine. Drop the hard requirement on + cheetah. This helps in python3 effort. (LP: #1219223) 0.7.5: - open 0.7.5 - Add a debug log message around import failures diff --git a/cloudinit/templater.py b/cloudinit/templater.py index 77af1270..02f6261d 100644 --- a/cloudinit/templater.py +++ b/cloudinit/templater.py @@ -20,13 +20,119 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from Cheetah.Template import Template +import collections +import re +try: + from Cheetah.Template import Template as CTemplate + CHEETAH_AVAILABLE = True +except (ImportError, AttributeError): + CHEETAH_AVAILABLE = False + +try: + import jinja2 + from jinja2 import Template as JTemplate + JINJA_AVAILABLE = True +except (ImportError, AttributeError): + JINJA_AVAILABLE = False + +from cloudinit import log as logging +from cloudinit import type_utils as tu from cloudinit import util +LOG = logging.getLogger(__name__) +TYPE_MATCHER = re.compile(r"##\s*template:(.*)", re.I) +BASIC_MATCHER = re.compile(r'\$\{([A-Za-z0-9_.]+)\}|\$([A-Za-z0-9_.]+)') + + +def basic_render(content, params): + """This does simple replacement of bash variable like templates. + + It identifies patterns like ${a} or $a and can also identify patterns like + ${a.b} or $a.b which will look for a key 'b' in the dictionary rooted + by key 'a'. + """ + + def replacer(match): + # Only 1 of the 2 groups will actually have a valid entry. + name = match.group(1) + if name is None: + name = match.group(2) + if name is None: + raise RuntimeError("Match encountered but no valid group present") + path = collections.deque(name.split(".")) + selected_params = params + while len(path) > 1: + key = path.popleft() + if not isinstance(selected_params, dict): + raise TypeError("Can not traverse into" + " non-dictionary '%s' of type %s while" + " looking for subkey '%s'" + % (selected_params, + tu.obj_name(selected_params), + key)) + selected_params = selected_params[key] + key = path.popleft() + if not isinstance(selected_params, dict): + raise TypeError("Can not extract key '%s' from non-dictionary" + " '%s' of type %s" + % (key, selected_params, + tu.obj_name(selected_params))) + return str(selected_params[key]) + + return BASIC_MATCHER.sub(replacer, content) + + +def detect_template(text): + + def cheetah_render(content, params): + return CTemplate(content, searchList=[params]).respond() + + def jinja_render(content, params): + return JTemplate(content, + undefined=jinja2.StrictUndefined, + trim_blocks=True).render(**params) + + if text.find("\n") != -1: + ident, rest = text.split("\n", 1) + else: + ident = text + rest = '' + type_match = TYPE_MATCHER.match(ident) + if not type_match: + if not CHEETAH_AVAILABLE: + LOG.warn("Cheetah not available as the default renderer for" + " unknown template, reverting to the basic renderer.") + return ('basic', basic_render, text) + else: + return ('cheetah', cheetah_render, text) + else: + template_type = type_match.group(1).lower().strip() + if template_type not in ('jinja', 'cheetah', 'basic'): + raise ValueError("Unknown template rendering type '%s' requested" + % template_type) + if template_type == 'jinja' and not JINJA_AVAILABLE: + LOG.warn("Jinja not available as the selected renderer for" + " desired template, reverting to the basic renderer.") + return ('basic', basic_render, rest) + elif template_type == 'jinja' and JINJA_AVAILABLE: + return ('jinja', jinja_render, rest) + if template_type == 'cheetah' and not CHEETAH_AVAILABLE: + LOG.warn("Cheetah not available as the selected renderer for" + " desired template, reverting to the basic renderer.") + return ('basic', basic_render, rest) + elif template_type == 'cheetah' and CHEETAH_AVAILABLE: + return ('cheetah', cheetah_render, rest) + # Only thing left over is the basic renderer (it is always available). + return ('basic', basic_render, rest) + def render_from_file(fn, params): - return render_string(util.load_file(fn), params) + if not params: + params = {} + template_type, renderer, content = detect_template(util.load_file(fn)) + LOG.debug("Rendering content of '%s' using renderer %s", fn, template_type) + return renderer(content, params) def render_to_file(fn, outfn, params, mode=0644): @@ -37,4 +143,5 @@ def render_to_file(fn, outfn, params, mode=0644): def render_string(content, params): if not params: params = {} - return Template(content, searchList=[params]).respond() + template_type, renderer, content = detect_template(content) + return renderer(content, params) diff --git a/packages/bddeb b/packages/bddeb index 9552aa40..5c538739 100755 --- a/packages/bddeb +++ b/packages/bddeb @@ -31,6 +31,7 @@ PKG_MP = { 'argparse': 'python-argparse', 'cheetah': 'python-cheetah', 'configobj': 'python-configobj', + 'jinja2': 'python-jinja2', 'jsonpatch': 'python-jsonpatch | python-json-patch', 'oauth': 'python-oauth', 'prettytable': 'python-prettytable', diff --git a/packages/brpm b/packages/brpm index f8ba1db1..b8bbff9d 100755 --- a/packages/brpm +++ b/packages/brpm @@ -37,6 +37,7 @@ PKG_MP = { 'redhat': { 'argparse': 'python-argparse', 'cheetah': 'python-cheetah', + 'jinja2': 'python-jinja2', 'configobj': 'python-configobj', 'jsonpatch': 'python-jsonpatch', 'oauth': 'python-oauth', diff --git a/requirements.txt b/requirements.txt index fdcbd143..943dbef7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ # Used for untemplating any files or strings with parameters. cheetah +jinja2 # This is used for any pretty printing of tabular data. PrettyTable diff --git a/templates/chef_client.rb.tmpl b/templates/chef_client.rb.tmpl index 7981cba7..538850ca 100644 --- a/templates/chef_client.rb.tmpl +++ b/templates/chef_client.rb.tmpl @@ -1,25 +1,25 @@ -#* - This file is only utilized if the module 'cc_chef' is enabled in - cloud-config. Specifically, in order to enable it - you need to add the following to config: - chef: - validation_key: XYZ - validation_cert: XYZ - validation_name: XYZ - server_url: XYZ -*# +## template:jinja +{# +This file is only utilized if the module 'cc_chef' is enabled in +cloud-config. Specifically, in order to enable it +you need to add the following to config: + chef: + validation_key: XYZ + validation_cert: XYZ + validation_name: XYZ + server_url: XYZ +-#} log_level :info log_location "/var/log/chef/client.log" ssl_verify_mode :verify_none -validation_client_name "$validation_name" +validation_client_name "{{validation_name}}" validation_key "/etc/chef/validation.pem" client_key "/etc/chef/client.pem" -chef_server_url "$server_url" -environment "$environment" -node_name "$node_name" +chef_server_url "{{server_url}}" +environment "{{environment}}" +node_name "{{node_name}}" json_attribs "/etc/chef/firstboot.json" file_cache_path "/var/cache/chef" file_backup_path "/var/backups/chef" pid_file "/var/run/chef/client.pid" Chef::Log::Formatter.show_time = true - diff --git a/templates/hosts.debian.tmpl b/templates/hosts.debian.tmpl index ae120b02..a1d97212 100644 --- a/templates/hosts.debian.tmpl +++ b/templates/hosts.debian.tmpl @@ -1,19 +1,19 @@ -## This file (/etc/cloud/templates/hosts.tmpl) is only utilized -## if enabled in cloud-config. Specifically, in order to enable it -## you need to add the following to config: -## manage_etc_hosts: True -## -## Note, double-hash commented lines will not appear in /etc/hosts -# +## template:jinja +{# +This file (/etc/cloud/templates/hosts.tmpl) is only utilized +if enabled in cloud-config. Specifically, in order to enable it +you need to add the following to config: + manage_etc_hosts: True +-#} # Your system has configured 'manage_etc_hosts' as True. # As a result, if you wish for changes to this file to persist # then you will need to either # a.) make changes to the master file in /etc/cloud/templates/hosts.tmpl # b.) change or remove the value of 'manage_etc_hosts' in # /etc/cloud/cloud.cfg or cloud-config from user-data -# -## The value '$hostname' will be replaced with the local-hostname -127.0.1.1 $fqdn $hostname +# +{# The value '{{hostname}}' will be replaced with the local-hostname -#} +127.0.1.1 {{fqdn}} {{hostname}} 127.0.0.1 localhost # The following lines are desirable for IPv6 capable hosts @@ -23,3 +23,4 @@ ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters ff02::3 ip6-allhosts + diff --git a/templates/hosts.redhat.tmpl b/templates/hosts.redhat.tmpl index 80459d95..bc5da32c 100644 --- a/templates/hosts.redhat.tmpl +++ b/templates/hosts.redhat.tmpl @@ -1,9 +1,10 @@ -#* - This file /etc/cloud/templates/hosts.redhat.tmpl is only utilized - if enabled in cloud-config. Specifically, in order to enable it - you need to add the following to config: - manage_etc_hosts: True -*# +## template:jinja +{# +This file /etc/cloud/templates/hosts.redhat.tmpl is only utilized +if enabled in cloud-config. Specifically, in order to enable it +you need to add the following to config: + manage_etc_hosts: True +-#} # Your system has configured 'manage_etc_hosts' as True. # As a result, if you wish for changes to this file to persist # then you will need to either @@ -12,12 +13,12 @@ # /etc/cloud/cloud.cfg or cloud-config from user-data # # The following lines are desirable for IPv4 capable hosts -127.0.0.1 ${fqdn} ${hostname} +127.0.0.1 {{fqdn}} {{hostname}} 127.0.0.1 localhost.localdomain localhost 127.0.0.1 localhost4.localdomain4 localhost4 # The following lines are desirable for IPv6 capable hosts -::1 ${fqdn} ${hostname} +::1 {{fqdn}} {{hostname}} ::1 localhost.localdomain localhost ::1 localhost6.localdomain6 localhost6 diff --git a/templates/hosts.suse.tmpl b/templates/hosts.suse.tmpl index 5d3d57e4..b6082692 100644 --- a/templates/hosts.suse.tmpl +++ b/templates/hosts.suse.tmpl @@ -1,9 +1,10 @@ -#* - This file /etc/cloud/templates/hosts.suse.tmpl is only utilized - if enabled in cloud-config. Specifically, in order to enable it - you need to add the following to config: - manage_etc_hosts: True -*# +## template:jinja +{# +This file /etc/cloud/templates/hosts.suse.tmpl is only utilized +if enabled in cloud-config. Specifically, in order to enable it +you need to add the following to config: + manage_etc_hosts: True +-#} # Your system has configured 'manage_etc_hosts' as True. # As a result, if you wish for changes to this file to persist # then you will need to either @@ -22,3 +23,4 @@ ff00::0 ipv6-mcastprefix ff02::1 ipv6-allnodes ff02::2 ipv6-allrouters ff02::3 ipv6-allhosts + diff --git a/templates/resolv.conf.tmpl b/templates/resolv.conf.tmpl index b7e97b13..6f908f30 100644 --- a/templates/resolv.conf.tmpl +++ b/templates/resolv.conf.tmpl @@ -1,39 +1,30 @@ -# +## template:jinja # Your system has been configured with 'manage-resolv-conf' set to true. # As a result, cloud-init has written this file with configuration data # that it has been provided. Cloud-init, by default, will write this file # a single time (PER_ONCE). # +{% if nameservers is defined %} +{% for server in nameservers %} +nameserver {{server}} +{% endfor %} -#if $varExists('nameservers') -#for $server in $nameservers -nameserver $server -#end for -#end if -#if $varExists('searchdomains') -search #slurp -#for $search in $searchdomains -$search #slurp -#end for +{% endif -%} +{% if searchdomains is defined %} +search {% for search in searchdomains %}{{search}} {% endfor %} -#end if -#if $varExists('domain') -domain $domain -#end if -#if $varExists('sortlist') -sortlist #slurp -#for $sort in $sortlist -$sort #slurp -#end for +{% endif %} +{% if domain is defined %} +domain {{domain}} +{% endif %} +{% if sortlist is defined %} -#end if -#if $varExists('options') or $varExists('flags') -options #slurp -#for $flag in $flags -$flag #slurp -#end for -#for $key, $value in $options.items() -$key:$value #slurp -#end for +sortlist {% for sort in sortlist %}{{sort}} {% endfor %} +{% endif %} +{% if options is defined or flags is defined %} -#end if +options {% for flag in flags %}{{flag}} {% endfor %} +{% for key, value in options.iteritems() -%} + {{key}}:{{value}} +{% endfor %} +{% endif %} diff --git a/templates/sources.list.debian.tmpl b/templates/sources.list.debian.tmpl index 609bc6bd..c8043f76 100644 --- a/templates/sources.list.debian.tmpl +++ b/templates/sources.list.debian.tmpl @@ -1,28 +1,32 @@ -\## Note, this file is written by cloud-init on first boot of an instance -\## modifications made here will not survive a re-bundle. -\## if you wish to make changes you can: -\## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg -\## or do the same in user-data -\## b.) add sources in /etc/apt/sources.list.d -\## c.) make changes to template file /etc/cloud/templates/sources.list.debian.tmpl -\### +## template:jinja +## Note, this file is written by cloud-init on first boot of an instance +## modifications made here will not survive a re-bundle. +## if you wish to make changes you can: +## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg +## or do the same in user-data +## b.) add sources in /etc/apt/sources.list.d +## c.) make changes to template file /etc/cloud/templates/sources.list.debian.tmpl +### # See http://www.debian.org/releases/stable/i386/release-notes/ch-upgrading.html # for how to upgrade to newer versions of the distribution. -deb $mirror $codename main contrib non-free -deb-src $mirror $codename main contrib non-free +deb {{mirror}} {{codename}} main contrib non-free +deb-src {{mirror}} {{codename}} main contrib non-free -\## Major bug fix updates produced after the final release of the -\## distribution. -deb $security $codename/updates main contrib non-free -deb-src $security $codename/updates main contrib non-free -deb $mirror $codename-updates main contrib non-free -deb-src $mirror $codename-updates main contrib non-free +## Major bug fix updates produced after the final release of the +## distribution. +deb {{security}} {{codename}}/updates main contrib non-free +deb-src {{security}} {{codename}}/updates main contrib non-free +deb {{mirror}} {{codename}}-updates main contrib non-free +deb-src {{mirror}} {{codename}}-updates main contrib non-free -\## Uncomment the following two lines to add software from the 'backports' -\## repository. -\## N.B. software from this repository may not have been tested as -\## extensively as that contained in the main release, although it includes -\## newer versions of some applications which may provide useful features. -# deb http://backports.debian.org/debian-backports $codename-backports main contrib non-free -# deb-src http://backports.debian.org/debian-backports $codename-backports main contrib non-free +## Uncomment the following two lines to add software from the 'backports' +## repository. +## +## N.B. software from this repository may not have been tested as +## extensively as that contained in the main release, although it includes +## newer versions of some applications which may provide useful features. +{# +deb http://backports.debian.org/debian-backports {{codename}}-backports main contrib non-free +deb-src http://backports.debian.org/debian-backports {{codename}}-backports main contrib non-free +-#} diff --git a/templates/sources.list.ubuntu.tmpl b/templates/sources.list.ubuntu.tmpl index ce395b3d..4b1b019a 100644 --- a/templates/sources.list.ubuntu.tmpl +++ b/templates/sources.list.ubuntu.tmpl @@ -1,60 +1,60 @@ -\## Note, this file is written by cloud-init on first boot of an instance -\## modifications made here will not survive a re-bundle. -\## if you wish to make changes you can: -\## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg -\## or do the same in user-data -\## b.) add sources in /etc/apt/sources.list.d -\## c.) make changes to template file /etc/cloud/templates/sources.list.tmpl -\### +## template:jinja +## Note, this file is written by cloud-init on first boot of an instance +## modifications made here will not survive a re-bundle. +## if you wish to make changes you can: +## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg +## or do the same in user-data +## b.) add sources in /etc/apt/sources.list.d +## c.) make changes to template file /etc/cloud/templates/sources.list.tmpl # See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to # newer versions of the distribution. -deb $mirror $codename main -deb-src $mirror $codename main +deb {{mirror}} {{codename}} main +deb-src {{mirror}} {{codename}} main -\## Major bug fix updates produced after the final release of the -\## distribution. -deb $mirror $codename-updates main -deb-src $mirror $codename-updates main +## Major bug fix updates produced after the final release of the +## distribution. +deb {{mirror}} {{codename}}-updates main +deb-src {{mirror}} {{codename}}-updates main -\## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu -\## team. Also, please note that software in universe WILL NOT receive any -\## review or updates from the Ubuntu security team. -deb $mirror $codename universe -deb-src $mirror $codename universe -deb $mirror $codename-updates universe -deb-src $mirror $codename-updates universe +## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu +## team. Also, please note that software in universe WILL NOT receive any +## review or updates from the Ubuntu security team. +deb {{mirror}} {{codename}} universe +deb-src {{mirror}} {{codename}} universe +deb {{mirror}} {{codename}}-updates universe +deb-src {{mirror}} {{codename}}-updates universe -\## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu -\## team, and may not be under a free licence. Please satisfy yourself as to -\## your rights to use the software. Also, please note that software in -\## multiverse WILL NOT receive any review or updates from the Ubuntu -\## security team. -# deb $mirror $codename multiverse -# deb-src $mirror $codename multiverse -# deb $mirror $codename-updates multiverse -# deb-src $mirror $codename-updates multiverse +## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu +## team, and may not be under a free licence. Please satisfy yourself as to +## your rights to use the software. Also, please note that software in +## multiverse WILL NOT receive any review or updates from the Ubuntu +## security team. +# deb {{mirror}} {{codename}} multiverse +# deb-src {{mirror}} {{codename}} multiverse +# deb {{mirror}} {{codename}}-updates multiverse +# deb-src {{mirror}} {{codename}}-updates multiverse -\## Uncomment the following two lines to add software from the 'backports' -\## repository. -\## N.B. software from this repository may not have been tested as -\## extensively as that contained in the main release, although it includes -\## newer versions of some applications which may provide useful features. -\## Also, please note that software in backports WILL NOT receive any review -\## or updates from the Ubuntu security team. -# deb $mirror $codename-backports main restricted universe multiverse -# deb-src $mirror $codename-backports main restricted universe multiverse +## Uncomment the following two lines to add software from the 'backports' +## repository. +## N.B. software from this repository may not have been tested as +## extensively as that contained in the main release, although it includes +## newer versions of some applications which may provide useful features. +## Also, please note that software in backports WILL NOT receive any review +## or updates from the Ubuntu security team. +# deb {{mirror}} {{codename}}-backports main restricted universe multiverse +# deb-src {{mirror}} {{codename}}-backports main restricted universe multiverse -\## Uncomment the following two lines to add software from Canonical's -\## 'partner' repository. -\## This software is not part of Ubuntu, but is offered by Canonical and the -\## respective vendors as a service to Ubuntu users. -# deb http://archive.canonical.com/ubuntu $codename partner -# deb-src http://archive.canonical.com/ubuntu $codename partner +## Uncomment the following two lines to add software from Canonical's +## 'partner' repository. +## This software is not part of Ubuntu, but is offered by Canonical and the +## respective vendors as a service to Ubuntu users. +# deb http://archive.canonical.com/ubuntu {{codename}} partner +# deb-src http://archive.canonical.com/ubuntu {{codename}} partner -deb $security $codename-security main -deb-src $security $codename-security main -deb $security $codename-security universe -deb-src $security $codename-security universe -# deb $security $codename-security multiverse -# deb-src $security $codename-security multiverse +deb {{security}} {{codename}}-security main +deb-src {{security}} {{codename}}-security main +deb {{security}} {{codename}}-security universe +deb-src {{security}} {{codename}}-security universe +# deb {{security}} {{codename}}-security multiverse +# deb-src {{security}} {{codename}}-security multiverse diff --git a/tests/unittests/test_templating.py b/tests/unittests/test_templating.py new file mode 100644 index 00000000..1ec3004b --- /dev/null +++ b/tests/unittests/test_templating.py @@ -0,0 +1,107 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2014 Yahoo! Inc. +# +# Author: Joshua Harlow +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from tests.unittests import helpers as test_helpers +import textwrap + +from cloudinit import templater + + +class TestTemplates(test_helpers.TestCase): + def test_render_basic(self): + in_data = textwrap.dedent(""" + ${b} + + c = d + """) + in_data = in_data.strip() + expected_data = textwrap.dedent(""" + 2 + + c = d + """) + out_data = templater.basic_render(in_data, {'b': 2}) + self.assertEqual(expected_data.strip(), out_data) + + def test_detection(self): + blob = "## template:cheetah" + + (template_type, renderer, contents) = templater.detect_template(blob) + self.assertIn("cheetah", template_type) + self.assertEqual("", contents.strip()) + + blob = "blahblah $blah" + (template_type, renderer, contents) = templater.detect_template(blob) + self.assertIn("cheetah", template_type) + self.assertEquals(blob, contents) + + blob = '##template:something-new' + self.assertRaises(ValueError, templater.detect_template, blob) + + def test_render_cheetah(self): + blob = '''## template:cheetah +$a,$b''' + c = templater.render_string(blob, {"a": 1, "b": 2}) + self.assertEquals("1,2", c) + + def test_render_jinja(self): + blob = '''## template:jinja +{{a}},{{b}}''' + c = templater.render_string(blob, {"a": 1, "b": 2}) + self.assertEquals("1,2", c) + + def test_render_default(self): + blob = '''$a,$b''' + c = templater.render_string(blob, {"a": 1, "b": 2}) + self.assertEquals("1,2", c) + + def test_render_basic_deeper(self): + hn = 'myfoohost.yahoo.com' + expected_data = "h=%s\nc=d\n" % hn + in_data = "h=$hostname.canonical_name\nc=d\n" + params = { + "hostname": { + "canonical_name": hn, + }, + } + out_data = templater.render_string(in_data, params) + self.assertEqual(expected_data, out_data) + + def test_render_basic_no_parens(self): + hn = "myfoohost" + in_data = "h=$hostname\nc=d\n" + expected_data = "h=%s\nc=d\n" % hn + out_data = templater.basic_render(in_data, {'hostname': hn}) + self.assertEqual(expected_data, out_data) + + def test_render_basic_parens(self): + hn = "myfoohost" + in_data = "h = ${hostname}\nc=d\n" + expected_data = "h = %s\nc=d\n" % hn + out_data = templater.basic_render(in_data, {'hostname': hn}) + self.assertEqual(expected_data, out_data) + + def test_render_basic2(self): + mirror = "mymirror" + codename = "zany" + in_data = "deb $mirror $codename-updates main contrib non-free" + ex_data = "deb %s %s-updates main contrib non-free" % (mirror, codename) + + out_data = templater.basic_render(in_data, + {'mirror': mirror, 'codename': codename}) + self.assertEqual(ex_data, out_data)