4
3
from charmhelpers.core import host
5
4
from charmhelpers.core import hookenv
8
class ContextGenerator(object):
10
Base interface for template context container generators.
12
A template context is a dictionary that contains data needed to populate
13
the template. The generator instance should produce the context when
14
called (without arguments) by collecting information from juju (config-get,
15
relation-get, etc), the system, or whatever other sources are appropriate.
17
A context generator should only return any values if it has enough information
18
to provide all of its values. Any context that is missing data is considered
19
incomplete and will cause that template to not render until it has all of its
22
The template may receive several contexts, which will be merged together,
23
so care should be taken in the key names.
26
raise NotImplementedError
29
class StorableContext(object):
31
A mixin for persisting a context to disk.
33
def store_context(self, file_name, config_data):
34
with open(file_name, 'w') as file_stream:
35
yaml.dump(config_data, file_stream)
37
def read_context(self, file_name):
38
with open(file_name, 'r') as file_stream:
39
data = yaml.load(file_stream)
41
raise OSError("%s is empty" % file_name)
45
class ConfigContext(ContextGenerator):
47
A context generator that generates a context containing all of the
51
return hookenv.config()
54
class RelationContext(ContextGenerator):
56
Base class for a context generator that gets relation data from juju.
58
Subclasses must provide `interface`, which is the interface type of interest,
59
and `required_keys`, which is the set of keys required for the relation to
60
be considered complete. The first relation for the interface that is complete
61
will be used to populate the data for template.
63
The generated context will be namespaced under the interface type, to prevent
64
potential naming conflicts.
70
if not hookenv.relation_ids(self.interface):
74
for rid in hookenv.relation_ids(self.interface):
75
for unit in hookenv.related_units(rid):
76
reldata = hookenv.relation_get(rid=rid, unit=unit)
77
required = set(self.required_keys)
78
if set(reldata.keys()).issuperset(required):
79
ns = ctx.setdefault(self.interface, {})
80
for k, v in reldata.items():
87
class StaticContext(ContextGenerator):
88
def __init__(self, data):
95
def _collect_contexts(context_providers):
97
Helper function to collect and merge contexts from a list of providers.
99
If any of the contexts are incomplete (i.e., they return an empty dict),
100
the template is considered incomplete and will not render.
102
If there are no context_providers given, then it is automatically
103
considered complete and an empty context (`{}`) is returned.
105
if not context_providers:
106
return {} # special case: no contexts at all is always complete
108
for provider in context_providers:
116
def render(template_definitions, templates_dir=None):
118
Render one or more templates, given a list of template definitions.
120
The template definitions should be dicts with the keys: `source`, `target`,
121
`file_properties`, and `contexts`.
123
The `source` path, if not absolute, is relative to the `templates_dir`
124
given when the rendered was created. If `source` is not provided
125
for a template the `template_dir` will be consulted for
126
``basename(target).j2``.
7
def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None):
11
The `source` path, if not absolute, is relative to the `templates_dir`.
128
13
The `target` path should be absolute.
130
The `file_properties` should be a dict optionally containing
131
`owner`, `group`, or `perms` options, to be passed to `write_file`.
133
The `contexts` should be a list containing zero or more ContextGenerators.
135
The `template_dir` defaults to `$CHARM_DIR/templates`
137
Returns True if all of the templates were "complete" (i.e., the context
138
generators were able to collect the information needed to render the
139
template) and were rendered.
141
Note: If a template has no contexts listed, it is automatically considered
15
The context should be a dict containing the values to be replaced in the
18
The `owner`, `group`, and `perms` options will be passed to `write_file`.
20
If omitted, `templates_dir` defaults to the `templates` folder in the charm.
22
Note: Using this requires python-jinja2; if it is not installed, calling
23
this will attempt to use charmhelpers.fetch.apt_install to install it.
144
# lazy import jinja2 in case templating is needed in install hook
145
from jinja2 import FileSystemLoader, Environment, exceptions
26
from jinja2 import FileSystemLoader, Environment, exceptions
29
from charmhelpers.fetch import apt_install
31
hookenv.log('Could not import jinja2, and could not import '
32
'charmhelpers.fetch to install it',
35
apt_install('python-jinja2', fatal=True)
36
from jinja2 import FileSystemLoader, Environment, exceptions
147
38
if templates_dir is None:
148
39
templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
149
40
loader = Environment(loader=FileSystemLoader(templates_dir))
150
for tmpl in template_definitions:
151
ctx = _collect_contexts(tmpl.get('contexts', []))
156
source = tmpl.get('source', os.path.basename(tmpl['target'])+'.j2')
157
template = loader.get_template(source)
158
except exceptions.TemplateNotFound as e:
159
hookenv.log('Could not load template %s from %s.' %
160
(tmpl['source'], templates_dir),
163
content = template.render(ctx)
164
host.mkdir(os.path.dirname(tmpl['target']))
165
host.write_file(tmpl['target'], content, **tmpl.get('file_properties', {}))
43
template = loader.get_template(source)
44
except exceptions.TemplateNotFound as e:
45
hookenv.log('Could not load template %s from %s.' %
46
(source, templates_dir),
49
content = template.render(context)
50
host.mkdir(os.path.dirname(target))
51
host.write_file(target, content, owner, group, perms)