diff options
| -rw-r--r-- | cloudinit/templater.py | 107 | ||||
| -rw-r--r-- | tests/unittests/test_templating.py | 24 | 
2 files changed, 100 insertions, 31 deletions
| diff --git a/cloudinit/templater.py b/cloudinit/templater.py index 9922c633..459f241a 100644 --- a/cloudinit/templater.py +++ b/cloudinit/templater.py @@ -20,30 +20,72 @@  #    You should have received a copy of the GNU General Public License  #    along with this program.  If not, see <http://www.gnu.org/licenses/>. +import collections  import re -from Cheetah.Template import Template as CTemplate +try: +    from Cheetah.Template import Template as CTemplate +    CHEETAH_AVAILABLE = True +except (ImportError, AttributeError): +    CHEETAH_AVAILABLE = False -import jinja2 -from jinja2 import Template as JTemplate +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__) -DEF_RENDERER = 'cheetah' -RENDERERS = { -    'jinja': (lambda content, params: -              JTemplate(content, -                        undefined=jinja2.StrictUndefined, -                        trim_blocks=True).render(**params)), -    'cheetah': (lambda content, params: -                CTemplate(content, searchList=[params]).respond()), -}  TYPE_MATCHER = re.compile(r"##\s*template:(.*)", re.I) +BASIC_MATCHER = re.compile(r'\$\{([A-Za-z0-9_.]+)\}') + + +def basic_render(content, params): +    """This does simple replacement of bash variable like templates. + +    It identifies patterns like ${a} and can also identify patterns like +    ${a.b} which will look for a key 'b' in the dictionary rooted by key 'a'. +    """ + +    def replacer(match): +        path = collections.deque(match.group(1).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: @@ -51,14 +93,32 @@ def detect_template(text):          rest = ''      type_match = TYPE_MATCHER.match(ident)      if not type_match: -        return (DEF_RENDERER, text) -    template_type = type_match.group(1).lower().strip() -    if template_type not in RENDERERS: -        raise ValueError("Unknown template type '%s' requested" -                         % template_type) +        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: -        return (template_type, rest) -     +        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) @@ -72,10 +132,5 @@ def render_to_file(fn, outfn, params, mode=0644):  def render_string(content, params):      if not params:          params = {} -    try: -        renderer, content = detect_template(content) -    except ValueError as e: -        renderer = DEF_RENDERER -        LOG.warn("%s, using renderer %s", e, renderer) -    LOG.debug("Rendering %s using renderer '%s'", content, renderer) -    return RENDERERS[renderer](content, params) +    template_type, renderer, content = detect_template(content) +    return renderer(content, params) diff --git a/tests/unittests/test_templating.py b/tests/unittests/test_templating.py index b4f425a8..c3faac3d 100644 --- a/tests/unittests/test_templating.py +++ b/tests/unittests/test_templating.py @@ -18,21 +18,35 @@  from tests.unittests import helpers as test_helpers -  from cloudinit import templater  class TestTemplates(test_helpers.TestCase): +    def test_render_basic(self): +        in_data = """ +${b} + +c = d +""" +        in_data = in_data.strip() +        expected_data = """ +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, contents) = templater.detect_template(blob) -        self.assertEqual("cheetah", template_type) +        (template_type, renderer, contents) = templater.detect_template(blob) +        self.assertIn("cheetah", template_type)          self.assertEqual("", contents.strip())          blob = "blahblah $blah" -        (template_type, contents) = templater.detect_template(blob) -        self.assertEqual("cheetah", template_type) +        (template_type, renderer, contents) = templater.detect_template(blob) +        self.assertIn("cheetah", template_type)          self.assertEquals(blob, contents)          blob = '##template:something-new' | 
