~cloud-init-dev/cloud-init/trunk

« back to all changes in this revision

Viewing changes to cloudinit/templater.py

  • Committer: Scott Moser
  • Date: 2016-08-10 15:06:15 UTC
  • Revision ID: smoser@ubuntu.com-20160810150615-ma2fv107w3suy1ma
README: Mention move of revision control to git.

cloud-init development has moved its revision control to git.
It is available at 
  https://code.launchpad.net/cloud-init

Clone with 
  git clone https://git.launchpad.net/cloud-init
or
  git clone git+ssh://git.launchpad.net/cloud-init

For more information see
  https://git.launchpad.net/cloud-init/tree/HACKING.rst

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vi: ts=4 expandtab
2
 
#
3
 
#    Copyright (C) 2012 Canonical Ltd.
4
 
#    Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
5
 
#    Copyright (C) 2012 Yahoo! Inc.
6
 
#    Copyright (C) 2016 Amazon.com, Inc. or its affiliates.
7
 
#
8
 
#    Author: Scott Moser <scott.moser@canonical.com>
9
 
#    Author: Juerg Haefliger <juerg.haefliger@hp.com>
10
 
#    Author: Joshua Harlow <harlowja@yahoo-inc.com>
11
 
#    Author: Andrew Jorgensen <ajorgens@amazon.com>
12
 
#
13
 
#    This program is free software: you can redistribute it and/or modify
14
 
#    it under the terms of the GNU General Public License version 3, as
15
 
#    published by the Free Software Foundation.
16
 
#
17
 
#    This program is distributed in the hope that it will be useful,
18
 
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
 
#    GNU General Public License for more details.
21
 
#
22
 
#    You should have received a copy of the GNU General Public License
23
 
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
24
 
 
25
 
import collections
26
 
import re
27
 
 
28
 
try:
29
 
    from Cheetah.Template import Template as CTemplate
30
 
    CHEETAH_AVAILABLE = True
31
 
except (ImportError, AttributeError):
32
 
    CHEETAH_AVAILABLE = False
33
 
 
34
 
try:
35
 
    import jinja2
36
 
    from jinja2 import Template as JTemplate
37
 
    JINJA_AVAILABLE = True
38
 
except (ImportError, AttributeError):
39
 
    JINJA_AVAILABLE = False
40
 
 
41
 
from cloudinit import log as logging
42
 
from cloudinit import type_utils as tu
43
 
from cloudinit import util
44
 
 
45
 
LOG = logging.getLogger(__name__)
46
 
TYPE_MATCHER = re.compile(r"##\s*template:(.*)", re.I)
47
 
BASIC_MATCHER = re.compile(r'\$\{([A-Za-z0-9_.]+)\}|\$([A-Za-z0-9_.]+)')
48
 
 
49
 
 
50
 
def basic_render(content, params):
51
 
    """This does simple replacement of bash variable like templates.
52
 
 
53
 
    It identifies patterns like ${a} or $a and can also identify patterns like
54
 
    ${a.b} or $a.b which will look for a key 'b' in the dictionary rooted
55
 
    by key 'a'.
56
 
    """
57
 
 
58
 
    def replacer(match):
59
 
        # Only 1 of the 2 groups will actually have a valid entry.
60
 
        name = match.group(1)
61
 
        if name is None:
62
 
            name = match.group(2)
63
 
        if name is None:
64
 
            raise RuntimeError("Match encountered but no valid group present")
65
 
        path = collections.deque(name.split("."))
66
 
        selected_params = params
67
 
        while len(path) > 1:
68
 
            key = path.popleft()
69
 
            if not isinstance(selected_params, dict):
70
 
                raise TypeError("Can not traverse into"
71
 
                                " non-dictionary '%s' of type %s while"
72
 
                                " looking for subkey '%s'"
73
 
                                % (selected_params,
74
 
                                   tu.obj_name(selected_params),
75
 
                                   key))
76
 
            selected_params = selected_params[key]
77
 
        key = path.popleft()
78
 
        if not isinstance(selected_params, dict):
79
 
            raise TypeError("Can not extract key '%s' from non-dictionary"
80
 
                            " '%s' of type %s"
81
 
                            % (key, selected_params,
82
 
                               tu.obj_name(selected_params)))
83
 
        return str(selected_params[key])
84
 
 
85
 
    return BASIC_MATCHER.sub(replacer, content)
86
 
 
87
 
 
88
 
def detect_template(text):
89
 
 
90
 
    def cheetah_render(content, params):
91
 
        return CTemplate(content, searchList=[params]).respond()
92
 
 
93
 
    def jinja_render(content, params):
94
 
        # keep_trailing_newline is in jinja2 2.7+, not 2.6
95
 
        add = "\n" if content.endswith("\n") else ""
96
 
        return JTemplate(content,
97
 
                         undefined=jinja2.StrictUndefined,
98
 
                         trim_blocks=True).render(**params) + add
99
 
 
100
 
    if text.find("\n") != -1:
101
 
        ident, rest = text.split("\n", 1)
102
 
    else:
103
 
        ident = text
104
 
        rest = ''
105
 
    type_match = TYPE_MATCHER.match(ident)
106
 
    if not type_match:
107
 
        if CHEETAH_AVAILABLE:
108
 
            LOG.debug("Using Cheetah as the renderer for unknown template.")
109
 
            return ('cheetah', cheetah_render, text)
110
 
        else:
111
 
            return ('basic', basic_render, text)
112
 
    else:
113
 
        template_type = type_match.group(1).lower().strip()
114
 
        if template_type not in ('jinja', 'cheetah', 'basic'):
115
 
            raise ValueError("Unknown template rendering type '%s' requested"
116
 
                             % template_type)
117
 
        if template_type == 'jinja' and not JINJA_AVAILABLE:
118
 
            LOG.warn("Jinja not available as the selected renderer for"
119
 
                     " desired template, reverting to the basic renderer.")
120
 
            return ('basic', basic_render, rest)
121
 
        elif template_type == 'jinja' and JINJA_AVAILABLE:
122
 
            return ('jinja', jinja_render, rest)
123
 
        if template_type == 'cheetah' and not CHEETAH_AVAILABLE:
124
 
            LOG.warn("Cheetah not available as the selected renderer for"
125
 
                     " desired template, reverting to the basic renderer.")
126
 
            return ('basic', basic_render, rest)
127
 
        elif template_type == 'cheetah' and CHEETAH_AVAILABLE:
128
 
            return ('cheetah', cheetah_render, rest)
129
 
        # Only thing left over is the basic renderer (it is always available).
130
 
        return ('basic', basic_render, rest)
131
 
 
132
 
 
133
 
def render_from_file(fn, params):
134
 
    if not params:
135
 
        params = {}
136
 
    template_type, renderer, content = detect_template(util.load_file(fn))
137
 
    LOG.debug("Rendering content of '%s' using renderer %s", fn, template_type)
138
 
    return renderer(content, params)
139
 
 
140
 
 
141
 
def render_to_file(fn, outfn, params, mode=0o644):
142
 
    contents = render_from_file(fn, params)
143
 
    util.write_file(outfn, contents, mode=mode)
144
 
 
145
 
 
146
 
def render_string_to_file(content, outfn, params, mode=0o644):
147
 
    contents = render_string(content, params)
148
 
    util.write_file(outfn, contents, mode=mode)
149
 
 
150
 
 
151
 
def render_string(content, params):
152
 
    if not params:
153
 
        params = {}
154
 
    template_type, renderer, content = detect_template(content)
155
 
    return renderer(content, params)