~johnsca/charms/trusty/cf-cloud-controller/services-callback-fu

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/core/templating.py

  • Committer: Cory Johns
  • Date: 2014-05-26 20:43:13 UTC
  • Revision ID: cory.johns@canonical.com-20140526204313-1rk5xxv2vokp1z8t
Use refactored services API and remove symlink hooks

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
import os
2
 
import yaml
3
2
 
4
3
from charmhelpers.core import host
5
4
from charmhelpers.core import hookenv
6
5
 
7
6
 
8
 
class ContextGenerator(object):
9
 
    """
10
 
    Base interface for template context container generators.
11
 
 
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.
16
 
 
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
20
 
    necessary data.
21
 
 
22
 
    The template may receive several contexts, which will be merged together,
23
 
    so care should be taken in the key names.
24
 
    """
25
 
    def __call__(self):
26
 
        raise NotImplementedError
27
 
 
28
 
 
29
 
class StorableContext(object):
30
 
    """
31
 
    A mixin for persisting a context to disk.
32
 
    """
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)
36
 
 
37
 
    def read_context(self, file_name):
38
 
        with open(file_name, 'r') as file_stream:
39
 
            data = yaml.load(file_stream)
40
 
            if not data:
41
 
                raise OSError("%s is empty" % file_name)
42
 
            return data
43
 
 
44
 
 
45
 
class ConfigContext(ContextGenerator):
46
 
    """
47
 
    A context generator that generates a context containing all of the
48
 
    juju config values.
49
 
    """
50
 
    def __call__(self):
51
 
        return hookenv.config()
52
 
 
53
 
 
54
 
class RelationContext(ContextGenerator):
55
 
    """
56
 
    Base class for a context generator that gets relation data from juju.
57
 
 
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.
62
 
 
63
 
    The generated context will be namespaced under the interface type, to prevent
64
 
    potential naming conflicts.
65
 
    """
66
 
    interface = None
67
 
    required_keys = []
68
 
 
69
 
    def __call__(self):
70
 
        if not hookenv.relation_ids(self.interface):
71
 
            return {}
72
 
 
73
 
        ctx = {}
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():
81
 
                        ns[k] = v
82
 
                    return ctx
83
 
 
84
 
        return {}
85
 
 
86
 
 
87
 
class StaticContext(ContextGenerator):
88
 
    def __init__(self, data):
89
 
        self.data = data
90
 
 
91
 
    def __call__(self):
92
 
        return self.data
93
 
 
94
 
 
95
 
def _collect_contexts(context_providers):
96
 
    """
97
 
    Helper function to collect and merge contexts from a list of providers.
98
 
 
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.
101
 
 
102
 
    If there are no context_providers given, then it is automatically
103
 
    considered complete and an empty context (`{}`) is returned.
104
 
    """
105
 
    if not context_providers:
106
 
        return {}  # special case: no contexts at all is always complete
107
 
    ctx = {}
108
 
    for provider in context_providers:
109
 
        c = provider()
110
 
        if not c:
111
 
            return False
112
 
        ctx.update(c)
113
 
    return ctx
114
 
 
115
 
 
116
 
def render(template_definitions, templates_dir=None):
117
 
    """
118
 
    Render one or more templates, given a list of template definitions.
119
 
 
120
 
    The template definitions should be dicts with the keys: `source`, `target`,
121
 
    `file_properties`, and `contexts`.
122
 
 
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):
 
8
    """
 
9
    Render a template.
 
10
 
 
11
    The `source` path, if not absolute, is relative to the `templates_dir`.
127
12
 
128
13
    The `target` path should be absolute.
129
14
 
130
 
    The `file_properties` should be a dict optionally containing
131
 
    `owner`, `group`, or `perms` options, to be passed to `write_file`.
132
 
 
133
 
    The `contexts` should be a list containing zero or more ContextGenerators.
134
 
 
135
 
    The `template_dir` defaults to `$CHARM_DIR/templates`
136
 
 
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.
140
 
 
141
 
    Note: If a template has no contexts listed, it is automatically considered
142
 
    complete.
 
15
    The context should be a dict containing the values to be replaced in the
 
16
    template.
 
17
 
 
18
    The `owner`, `group`, and `perms` options will be passed to `write_file`.
 
19
 
 
20
    If omitted, `templates_dir` defaults to the `templates` folder in the charm.
 
21
 
 
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.
143
24
    """
144
 
    # lazy import jinja2 in case templating is needed in install hook
145
 
    from jinja2 import FileSystemLoader, Environment, exceptions
146
 
    all_complete = True
 
25
    try:
 
26
        from jinja2 import FileSystemLoader, Environment, exceptions
 
27
    except ImportError:
 
28
        try:
 
29
            from charmhelpers.fetch import apt_install
 
30
        except ImportError:
 
31
            hookenv.log('Could not import jinja2, and could not import '
 
32
                        'charmhelpers.fetch to install it',
 
33
                        level=hookenv.ERROR)
 
34
            raise
 
35
        apt_install('python-jinja2', fatal=True)
 
36
        from jinja2 import FileSystemLoader, Environment, exceptions
 
37
 
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', []))
152
 
        if ctx is False:
153
 
            all_complete = False
154
 
            continue
155
 
        try:
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),
161
 
                        level=hookenv.ERROR)
162
 
            raise e
163
 
        content = template.render(ctx)
164
 
        host.mkdir(os.path.dirname(tmpl['target']))
165
 
        host.write_file(tmpl['target'], content, **tmpl.get('file_properties', {}))
166
 
    return all_complete
 
41
    try:
 
42
        source = source
 
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),
 
47
                    level=hookenv.ERROR)
 
48
        raise e
 
49
    content = template.render(context)
 
50
    host.mkdir(os.path.dirname(target))
 
51
    host.write_file(target, content, owner, group, perms)