1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
# This file is part of cloud-init. See LICENSE file for license information.
from errno import EACCES
import os
import re
try:
from jinja2.exceptions import UndefinedError as JUndefinedError
except ImportError:
# No jinja2 dependency
JUndefinedError = Exception
from cloudinit import handlers
from cloudinit import log as logging
from cloudinit.sources import INSTANCE_JSON_FILE
from cloudinit.templater import render_string, MISSING_JINJA_PREFIX
from cloudinit.util import b64d, load_file, load_json, json_dumps
from cloudinit.settings import PER_ALWAYS
LOG = logging.getLogger(__name__)
class JinjaTemplatePartHandler(handlers.Handler):
prefixes = ['## template: jinja']
def __init__(self, paths, **_kwargs):
handlers.Handler.__init__(self, PER_ALWAYS, version=3)
self.paths = paths
self.sub_handlers = {}
for handler in _kwargs.get('sub_handlers', []):
for ctype in handler.list_types():
self.sub_handlers[ctype] = handler
def handle_part(self, data, ctype, filename, payload, frequency, headers):
if ctype in handlers.CONTENT_SIGNALS:
return
jinja_json_file = os.path.join(self.paths.run_dir, INSTANCE_JSON_FILE)
rendered_payload = render_jinja_payload_from_file(
payload, filename, jinja_json_file)
if not rendered_payload:
return
subtype = handlers.type_from_starts_with(rendered_payload)
sub_handler = self.sub_handlers.get(subtype)
if not sub_handler:
LOG.warning(
'Ignoring jinja template for %s. Could not find supported'
' sub-handler for type %s', filename, subtype)
return
if sub_handler.handler_version == 3:
sub_handler.handle_part(
data, ctype, filename, rendered_payload, frequency, headers)
elif sub_handler.handler_version == 2:
sub_handler.handle_part(
data, ctype, filename, rendered_payload, frequency)
def render_jinja_payload_from_file(
payload, payload_fn, instance_data_file, debug=False):
"""Render a jinja template payload sourcing variables from jinja_vars_path.
@param payload: String of jinja template content. Should begin with
## template: jinja\n.
@param payload_fn: String representing the filename from which the payload
was read used in error reporting. Generally in part-handling this is
'part-##'.
@param instance_data_file: A path to a json file containing variables that
will be used as jinja template variables.
@return: A string of jinja-rendered content with the jinja header removed.
Returns None on error.
"""
instance_data = {}
rendered_payload = None
if not os.path.exists(instance_data_file):
raise RuntimeError(
'Cannot render jinja template vars. Instance data not yet'
' present at %s' % instance_data_file)
try:
instance_data = load_json(load_file(instance_data_file))
except (IOError, OSError) as e:
if e.errno == EACCES:
raise RuntimeError(
'Cannot render jinja template vars. No read permission on'
" '%s'. Try sudo" % instance_data_file)
rendered_payload = render_jinja_payload(
payload, payload_fn, instance_data, debug)
if not rendered_payload:
return None
return rendered_payload
def render_jinja_payload(payload, payload_fn, instance_data, debug=False):
instance_jinja_vars = convert_jinja_instance_data(
instance_data,
decode_paths=instance_data.get('base64-encoded-keys', []))
if debug:
LOG.debug('Converted jinja variables\n%s',
json_dumps(instance_jinja_vars))
try:
rendered_payload = render_string(payload, instance_jinja_vars)
except (TypeError, JUndefinedError) as e:
LOG.warning(
'Ignoring jinja template for %s: %s', payload_fn, str(e))
return None
warnings = [
"'%s'" % var.replace(MISSING_JINJA_PREFIX, '')
for var in re.findall(
r'%s[^\s]+' % MISSING_JINJA_PREFIX, rendered_payload)]
if warnings:
LOG.warning(
"Could not render jinja template variables in file '%s': %s",
payload_fn, ', '.join(warnings))
return rendered_payload
def convert_jinja_instance_data(data, prefix='', sep='/', decode_paths=()):
"""Process instance-data.json dict for use in jinja templates.
Replace hyphens with underscores for jinja templates and decode any
base64_encoded_keys.
"""
result = {}
decode_paths = [path.replace('-', '_') for path in decode_paths]
for key, value in sorted(data.items()):
if '-' in key:
# Standardize keys for use in #cloud-config/shell templates
key = key.replace('-', '_')
key_path = '{0}{1}{2}'.format(prefix, sep, key) if prefix else key
if key_path in decode_paths:
value = b64d(value)
if isinstance(value, dict):
result[key] = convert_jinja_instance_data(
value, key_path, sep=sep, decode_paths=decode_paths)
if re.match(r'v\d+', key):
# Copy values to top-level aliases
for subkey, subvalue in result[key].items():
result[subkey] = subvalue
else:
result[key] = value
return result
# vi: ts=4 expandtab
|